How to create a dockerized full-stack environment with MySQL, NestJS and NextJS

Gustavo Contreiras

Gustavo Contreiras

· 5 min read
Docker, MySQL, NestJS and NextJS logos

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:
    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 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. 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
Gustavo Contreiras

About Gustavo Contreiras

Fron-end | Back-end | DevOps

Copyright © 2024 DevTamarin. All rights reserved.
Powered by Vercel