How to create a dockerized full-stack environment with MySQL, NestJS and NextJS
Tags: docker, nextjs, nestjs, mysql
Published: 2023-02-08 11:14:03

## 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 folder `docker-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` and `package-lock.json` file into the app folder
- run `npm install`
- copy the rest of the content of our `nestjs-app` folder into the container’s `app` 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` to `dockerized-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` and `package-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&nbsp;
            <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>-&gt;</span>
            </h2>
            <p className={inter.className}>
              Find in-depth information about Next.js features and&nbsp;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>-&gt;</span>
            </h2>
            <p className={inter.className}>
              Learn about Next.js in an interactive course with&nbsp;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>-&gt;</span>
            </h2>
            <p className={inter.className}>
              Discover and deploy boilerplate example Next.js&nbsp;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>-&gt;</span>
            </h2>
            <p className={inter.className}>
              Instantly deploy your Next.js site to a shareable URL
              with&nbsp;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](https://github.com/GustavoContreiras/dockerized-full-stack-environment).
Please leave a ⭐ or ❤️ if it helped you.

## Useful links

- https://nextjs.org/docs/getting-started
- https://docs.nestjs.com/first-steps
- https://docs.nestjs.com/recipes/hot-reload
- https://docs.nestjs.com/recipes/sql-typeorm#sql-typeorm
- https://docs.nestjs.com/techniques/database
- https://hub.docker.com/_/mysql