DEV Community

Cover image for How to create a dockerized full-stack environment with MySQL, NestJS and NextJS
Gustavo Contreiras
Gustavo Contreiras

Posted on • Updated on

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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)
);
Enter fullscreen mode Exit fullscreen mode
  • Create the file 02-populate-users-table.sql
USE DOCKERIZED;
INSERT INTO users VALUES(1, "Test");
INSERT INTO users VALUES(2, "Test");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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,
    }
  };
};
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode
  • 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 {}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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
},
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
yahhamdii profile image
yahhamdii

Thank you

Collapse
 
bilalmohib profile image
Muhammad Bilal Mohib-ul-Nabi

Thank you so much

Collapse
 
codeofrelevancy profile image
Code of Relevancy

Great article

Collapse
 
thangvu2325 profile image
Vũ Đức Thắng

thank you. your tutorial is good for me <3