Introduction
This tutorial will teach you how to setup a dockerized environment that with a single command can run:
- a MySQL database server (and create and populate a database in it in the first run)
- a back-end using the NestJS framework with a simple API
- a front-end using the NextJS framework that calls the back-end API
If you have experience with Docker, NestJS and NextJS and want to skip explanations just to see it working you can go to the end of this article and clone the repository from GitHub.
Folder structure
You can see the most important files and it’s locations in the file tree below. Some files were hidden to make it easier to understand.
📦dockerized-full-stack-environment
┣ 📂mysql-db
┃ ┣ 📜00-create-db.sql
┃ ┣ 📜01-create-table-users.sql
┃ ┗ 📜02-populate-users-table.sql
┣ 📂nestjs-app
┃ ┣ 📂node_modules
┃ ┣ 📂src
┃ ┃ ┣ 📜app.module.ts
┃ ┃ ┣ 📜app.controler.ts
┃ ┃ ┣ 📜app.service.ts
┃ ┃ ┗ 📜main.ts
┃ ┣ 📂test
┃ ┣ 📜.dockerignore
┃ ┣ 📜Dockerfile
┃ ┣ 📜package.json
┃ ┗ 📜webpack-hmr.config.js
┣ 📂nextjs-app
┃ ┣ 📂node_modules
┃ ┣ 📂pages
┃ ┃ ┗📜index.ts
┃ ┣ 📂public
┃ ┣ 📂styles
┃ ┣ 📜.dockerignore
┃ ┣ 📜Dockerfile
┃ ┣ 📜package.json
┃ ┗ 📜next.config.js
┣ 📜.env
┣ 📜docker-compose.yml
┗ 📜package.json
Install Docker Desktop
To install it on Windows, first you will have to open the PowerShell as administrator and run wsl --install
to install the Windows Subsystem for Linux, by default it will install Ubuntu.
https://docs.docker.com/desktop/install/windows-install/
https://docs.docker.com/desktop/install/linux-install/
https://docs.docker.com/desktop/install/mac-install/
Create the project root folder
This folder will have:
- the database project
- the back-end project
- the front-end project
- the docker-compose script
- the package.json file with npm scripts that works as shortcuts for the docker-compose commands
- the .env file with ports used in the services and credentia for the database
mkdir dockerized-full-stack-environment
cd dockerized-full-stack-environment
Create package.json file
Create the file package.json
and paste the following content. The scripts
section contains npm scripts to start each of the projects.
{
"name": "dockerized-full-stack-environment",
"version": "1.0.0",
"description": "Dockerized environment with a database, a back-end and a front-end application.",
"scripts": {
"build:back": "docker-compose build nestjs-app",
"build:front": "docker-compose build nextjs-app",
"start:db": "docker-compose up mysql-db",
"start:back": "docker-compose up nestjs-app --renew-anon-volumes",
"start:front": "docker-compose up nextjs-app --renew-anon-volumes",
"clean": "docker-compose down -v"
},
"author": "",
"license": "ISC"
}
Create docker-compose.yml file
This file is a script that defines how the docker images are executed together in the same container. It has three services for each of our projects. Copy and paste the code below inside the file.
version: '3.8'
networks:
default:
services:
mysql-db:
# NOTE: Uncomment the line below if you are using Mac with m1/m2 chip
# platform: linux/x86_64
container_name: mysql-db
image: mysql:5.7
# NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
# (this is just an example, not intended to be a production configuration)
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
env_file: ./.env
environment:
MYSQL_ROOT_PASSWORD: $MYSQLDB_PASSWORD
ports:
- $MYSQLDB_LOCAL_PORT:$MYSQLDB_DOCKER_PORT
volumes:
- mysql-volume:/var/lib/mysql:rw
- ./mysql-db:/docker-entrypoint-initdb.d/
networks:
- default
nestjs-app:
container_name: nestjs-app
# depends_on:
# - mysql-db
build: ./nestjs-app
restart: unless-stopped
env_file: ./.env
ports:
- $NESTJS_APP_LOCAL_PORT:$NESTJS_APP_DOCKER_PORT
environment:
- DB_HOST=$MYSQLDB_HOST
- DB_USER=$MYSQLDB_USER
- DB_PASSWORD=$MYSQLDB_PASSWORD
- DB_DATABASE=$MYSQLDB_DATABASE
- DB_PORT=$MYSQLDB_DOCKER_PORT
stdin_open: true
tty: true
volumes:
- ./nestjs-app:/app
- /app/node_modules
networks:
- default
nextjs-app:
container_name: nextjs-app
# depends_on:
# - nestjs-app
build:
context: ./nextjs-app
dockerfile: Dockerfile
restart: unless-stopped
env_file: ./.env
ports:
- $NEXTJS_APP_LOCAL_PORT:$NEXTJS_APP_DOCKER_PORT
stdin_open: true
tty: true
volumes:
- ./nextjs-app:/app
- /app/node_modules
- /app/.next
networks:
- default
volumes:
mysql-volume:
In the mysql-db
service we are defining two volumes
:
- the first is to persist the database data after running and stopping the container execution
- the second is to link our folder
mysql-db
with the container’s folderdocker-entrypoint-initdb.d
. Any.sql
or.sh
file in this folder will be executed on the first run
In the nestjs-app
service we are linking our nestjs-app
folder with container's app
folder because this is necessary to make the hot-reload work.
And in nextjs-app
service we are doing the same thing we did for the nestjs-app
to make the hot-reload work.
Create .env file
Create the file .env
and paste the following content inside of it:
# In a docker container the host is the name of the service
MYSQLDB_HOST=mysql-db
MYSQLDB_USER=root
MYSQLDB_PASSWORD=root
MYSQLDB_DATABASE=DOCKERIZED
MYSQLDB_LOCAL_PORT=3307
MYSQLDB_DOCKER_PORT=3306
NESTJS_APP_LOCAL_PORT=3001
NESTJS_APP_DOCKER_PORT=3001
NEXTJS_APP_LOCAL_PORT=3000
NEXTJS_APP_DOCKER_PORT=3000
The MYSQLDB_LOCAL_PORT
is the port you will use in your Database Administrator Tool (in your computer) to access the database. In the docker-compose script we can see that our port 3307
will be mapped to the container’s port 3306
which is the port that MySQL is using in the container.
The back-end app will be available through the port 3001
and the front-end will be available in the port 3000
.
Create MySQL project
- Run
mkdir mysql-db
Add SQL scripts into it
Add files to the mysql-db
folder. They will be executed only once. Make sure you save them with UTF-8
encoding otherwise you will receive errors.
- Create the file
00-create-db.sql
CREATE DATABASE DOCKERIZED;
- Create the file
01-create-users-table.sql
USE DOCKERIZED;
CREATE TABLE users(
id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY(id)
);
- Create the file
02-populate-users-table.sql
USE DOCKERIZED;
INSERT INTO users VALUES(1, "Test");
INSERT INTO users VALUES(2, "Test");
After creating these files you can execute npm run start:db
to start the MySQL server and execute the queries.
Create NestJS project
Project startup
- Run
npm install -g @nestjs/cli
- Run
nest new nestjs-app
- Choose
npm
as package manager - Run
cd nestjs-app
- Run
npm install --save @nestjs/typeorm typeorm mysql2
We will use Nest’s integration with Typeorm package to make it easier and faster to implement the MySQL connection. - Run
npm install --save @nestjs/config
This package is necessary to load the.env
variables in the Nest way.
Create the Dockerfile
Create a file called Dockerfile
and add the following content to it:
FROM node:18-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
CMD npm run start:dev:docker
This is basically saying to:
- set (and create if it’s necessary) the app folder as the working directory
- copy the
package.json
andpackage-lock.json
file into the app folder - run
npm install
- copy the rest of the content of our
nestjs-app
folder into the container’sapp
folder - and run the command
npm run start:dev:docker
Hot-reload configurations for Webpack/Docker
- Run
npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
- Add the following
npm script
todockerized-full-stack-environment/nestjs-app/package.json
file:
"start:dev:docker": nest build --webpack --webpackPath webpack-hmr.config.js --watch
- Create the file
dockerized-full-stack-environment/nestjs-app/webpack-hmr.config.js
and add the following content to inside of it:
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
module.exports = function (options, webpack) {
return {
...options,
entry: ['webpack/hot/poll?100', options.entry],
externals: [
nodeExternals({
allowlist: ['webpack/hot/poll?100'],
}),
],
plugins: [
...options.plugins,
new webpack.HotModuleReplacementPlugin(),
new webpack.WatchIgnorePlugin({
paths: [/\.js$/, /\.d\.ts$/],
}),
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
],
watchOptions: {
poll: 1000,
aggregateTimeout: 300,
}
};
};
- We still have one more step to make the hot-reload work with Docker but we will do it in the next topic while implementing the API.
Implement the API
- Replace
dockerized-full-stack-environment/nestjs-app/src/main.ts
code with:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
declare const module: any;
async function bootstrap() {
const port = process.env.NESTJS_APP_DOCKER_PORT
const app = await NestFactory.create(AppModule);
// Enable CORS so we can access the application from a different origin
app.enableCors()
// Start the application
await app.listen(port).then((_value) => {
console.log(`Server started at http://localhost:${port}`)
});
// This is necessary to make the hot-reload work with Docker
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
- Replace
dockerized-full-stack-environment/nestjs-app/src/app.module.ts
code with:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
name: 'default',
type: 'mysql',
host: configService.get('DB_HOST'),
port: +configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [],
synchronize: false,
}),
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
What we are doing in this step is to load Nest’s ConfigModule (that loads the .env files) and then Nest’s TypeOrmModule (that starts the connection with the MySQL database).
- Replace
dockerized-full-stack-environment/nestjs-app/src/app.controler.ts
code with:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/users')
getUsers() {
return this.appService.getUsers();
}
}
In this step we are creating the GET endpoint /users that will return all the users from the users table in the database.
- Replace
dockerized-full-stack-environment/nestjs-app/src/app.service.ts
code with:
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class AppService {
constructor(private dataSource: DataSource) {}
getUsers() {
return this.dataSource.query('SELECT * FROM users');
}
}
And in this step we are getting our DataSource
(it was defined in the TypeOrmModule
in the app.module.ts
file) to call the database with a raw query.
At this point you should be able to run the NestJS app with npm run build:back
and npm run start:back
(executed from the folder dockerized-full-stack-environment) and see the users from the database in http://localhost:3001/users.
Create NextJS project
Project startup
- Run
npx create-next-app@latest --typescript
- Accept the installation of
create-next-app
package - Define the project name as
nextjs-app
- Press
enter
for the rest of questions
Hot-reload configurations for Webpack/Docker
- Add the following code to the file
dockerized-full-stack-environment/nextjs-app/next.config.js
inside of the object nextConfig:
webpack: config => {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
}
return config
},
Create the Dockerfile
# Dockerfile
# Use node alpine as it's a small node image
FROM node:alpine
# Create the directory on the node image
# where our Next.js app will live
RUN mkdir -p /app
# Set /app as the working directory
WORKDIR /app
# Copy package.json and package-lock.json
# to the /app working directory
COPY package*.json /app
# Install dependencies in /app
RUN yarn install
# Copy the rest of our Next.js folder into /app
COPY . /app
# Ensure port 3000 is accessible to our system
EXPOSE 3000
# Run yarn dev, as we would via the command line
CMD ["yarn", "dev"]
In this file we are basically doing the same thing we did for the NestJS project, we are:
- setting the working directory as
/app
- copying
package.json
andpackage-lock.json
into container’s/app
folder - running
yarn install
- copying the rest of the content from the folder
dockerized-full-stack-environment/nextjs
into container’s/app
folder - exposing the port
3000
- and finally executing the application with
yarn dev
Replace home page code to call back-end API
Open the file dockerized-full-stack-environment/extjs-app/src/pages/index.ts
and replace the content with:
import Head from 'next/head'
import Image from 'next/image'
import { Inter } from '@next/font/google'
import styles from '@/styles/Home.module.css'
import { useEffect, useState } from 'react'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('http://localhost:3001/users', {
headers: {
mode: 'cors'
}
})
.then((response: Response) => response.json())
.then((users: any) => {
setUsers(users)
})
}, [])
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.description}>
<p>
Get started by editing
<code className={styles.code}>pages/index.tsx</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className={styles.center}>
{users.map((user: any) => {
return (
<div key={`user-${user.id}`}>
<span>#{user.id} {user.name}</span><br />
</div>
)
})}
{/* <Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div className={styles.thirteen}>
<Image
src="/thirteen.svg"
alt="13"
width={40}
height={31}
priority
/>
</div> */}
</div>
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Docs <span>-></span>
</h2>
<p className={inter.className}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Learn <span>-></span>
</h2>
<p className={inter.className}>
Learn about Next.js in an interactive course with quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Templates <span>-></span>
</h2>
<p className={inter.className}>
Discover and deploy boilerplate example Next.js projects.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Deploy <span>-></span>
</h2>
<p className={inter.className}>
Instantly deploy your Next.js site to a shareable URL
with Vercel.
</p>
</a>
</div>
</main>
</>
)
}
You can see that we are using useEffect
to call http://localhost:3001/users
once and store it’s results in the users
variable. Then we are rendering these results in the UI.
At this point you can run npm run build:front
, npm run start:front
and access http://localhost:3000
to see the users
from the database.
Run everything together
Run docker-compose up
Run projects separeted
Run the mysql-db
npm run start:db
or docker-compose up mysql-db
Run the nestjs-app
npm run start:back
or docker-compose up nestjs-app
Run the nextjs-app
npm run start:front
or docker-compose upnextjs-app
Clean the database volume
Run npm run clean
or docker-compose down -v
Complete project
You can find the complete project in my GitHub repository.
Please leave a ⭐ or ❤️ if it helped you.
Top comments (5)
Thanks for sharing your knowledge 🙏
Thank you
Thank you so much
Great article
thank you. your tutorial is good for me <3