Docker MERN stack example

Tien Nguyen - Aug 23 '23 - - Dev Community

Docker provides lightweight containers to run services in isolation from our infrastructure so we can deliver software quickly. In this tutorial, I will show you how to dockerize MERN stack Application (React + Node.js + Express + MongoDB) example using Docker Compose and Nginx.

Related Posts:

MERN stack Application with Docker Overview

Assume that we have a fullstack React + Nodejs Express + MongoDB Application (MERN stack).
The problem is to containerize a system that requires more than one Docker container:

  • React for UI
  • Node.js Express for API
  • MongoDB for database

Docker Compose helps us setup the system more easily and efficiently than with only Docker. We're gonna following these steps:

  • Setup Nodejs App working with MongoDB database.
  • Create Dockerfile for Nodejs App.
  • Setup React App.
  • Create Dockerfile for React App.
  • Write Docker Compose configurations in YAML file.
  • Set Environment variables for Docker Compose
  • Run the system.

Directory Structure:

docker-mern-nginx-example-structure

Setup Nodejs App

You can read and get Github source code from one of following tutorials:

Using the code base above, we put the Nodejs project in bezkoder-api folder and modify some files to work with environment variables.

Firstly, let's add dotenv module into package.json.

{
  ...
  "dependencies": {
    "dotenv": "^10.0.0",
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Next we import dotenv in server.js and use process.env for setting up CORS and port.

require("dotenv").config();
...
var corsOptions = {
  origin: process.env.CLIENT_ORIGIN || "http://localhost:8081"
};

app.use(cors(corsOptions));
..
// set port, listen for requests
const PORT = process.env.NODE_DOCKER_PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});
Enter fullscreen mode Exit fullscreen mode

Then we change modify database configuration and initialization.

app/config/db.config.js

const {
  DB_USER,
  DB_PASSWORD,
  DB_HOST,
  DB_PORT,
  DB_NAME,
} = process.env;

module.exports = {
  url: `mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?authSource=admin`
};
Enter fullscreen mode Exit fullscreen mode

We also need to make a .env sample file that shows all necessary arguments.

bezkoder-api/.env.sample

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=123456
DB_NAME=bezkoder_db
DB_PORT=27017

NODE_DOCKER_PORT=8080

CLIENT_ORIGIN=http://127.0.0.1:8081
Enter fullscreen mode Exit fullscreen mode

Create Dockerfile for Nodejs App

Dockerfile defines a list of commands that Docker uses for setting up the Node.js application environment. So we put the file in bezkoder-api folder.

Because we will use Docker Compose, we won't define all the configuration commands in this Dockerfile.

bezkoder-api/Dockerfile

FROM node:14

WORKDIR /bezkoder-api
COPY package.json .
RUN npm install
COPY . .
CMD npm start
Enter fullscreen mode Exit fullscreen mode

Let me explain some points:

  • FROM: install the image of the Node.js version.
  • WORKDIR: path of the working directory.
  • COPY: copy package.json file to the container, then the second one copies all the files inside the project directory.
  • RUN: execute a command-line inside the container: npm install to install the dependencies in package.json.
  • CMD: run script npm start after the image is built.

Setup React App

You can read and get Github source code from one of following tutorials:

Using the code base above, we put the React project in bezkoder-ui folder and do some work.

Firstly, let's remove .env file because we're gonna work with environment variable from Docker.

Then we open http-common.js for updating baseURL of axios instance with process.env.REACT_APP_API_BASE_URL.

import axios from "axios";

export default axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080/api',
  headers: {
    "Content-type": "application/json"
  }
});
Enter fullscreen mode Exit fullscreen mode

Create Dockerfile for React App

We're gonna deploy the React app behind an Nginx server.

Same as Nodejs, we put Dockerfile inside bezkoder-ui folder.

bezkoder-upi/Dockerfile

# Stage 1
FROM node:14 as build-stage

WORKDIR /bezkoder-ui
COPY package.json .
RUN npm install
COPY . .

ARG REACT_APP_API_BASE_URL
ENV REACT_APP_API_BASE_URL=$REACT_APP_API_BASE_URL

RUN npm run build

# Stage 2
FROM nginx:1.17.0-alpine

COPY --from=build-stage /bezkoder-ui/build /usr/share/nginx/html
EXPOSE $REACT_DOCKER_PORT

CMD nginx -g 'daemon off;'
Enter fullscreen mode Exit fullscreen mode

There are two stage:

  • Stage 1: Build the React application

    • FROM: install the image of the Node.js version.
    • WORKDIR: path of the working directory.
    • COPY: copy package.json file to the container, then the second one copies all the files inside the project directory.
    • RUN: execute a command-line inside the container: npm install to install the dependencies in package.json.
    • ARG and ENV: get argument and set environment variable (prefix REACT_APP_ is required).
    • run script npm run build after the image is built, the product will be stored in build folder.
  • Stage 2: Serve the React application with Nginx

    • install the image of the nginx alpine version.
    • copy the react build from Stage 1 into /usr/share/nginx/html folder.
    • expose port (should be 80) to the Docker host.
    • daemon off; directive tells Nginx to stay in the foreground.

Write Docker Compose for MERN application

On the root of the project directory, we're gonna create the docker-compose.yml file for the MERN stack. Follow version 3 syntax defined by Docker:

version: '3.8'

services: 
    mongodb:
    bezkoder-api:
    bezkoder-ui:

volumes:

networks:
Enter fullscreen mode Exit fullscreen mode
  • version: Docker Compose file format version will be used.
  • services: individual services in isolated containers. Our application has three services: bezkoder-ui (React), bezkoder-api (Nodejs) and mongodb (MongoDB database).
  • volumes: named volumes that keeps our data alive after restart.
  • networks: facilitate communication between containers

Let's implement the details.

docker-compose.yml

version: '3.8'

services:
  mongodb:
    image: mongo:5.0.2
    restart: unless-stopped
    env_file: ./.env
    environment:
      - MONGO_INITDB_ROOT_USERNAME=$MONGODB_USER
      - MONGO_INITDB_ROOT_PASSWORD=$MONGODB_PASSWORD
    ports:
      - $MONGODB_LOCAL_PORT:$MONGODB_DOCKER_PORT
    volumes:
      - db:/data/db
    networks:
      - backend

  bezkoder-api:
    depends_on:
      - mongodb
    build: ./bezkoder-api
    restart: unless-stopped
    env_file: ./.env
    ports:
      - $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
    environment:
      - DB_HOST=mongodb
      - DB_USER=$MONGODB_USER
      - DB_PASSWORD=$MONGODB_PASSWORD
      - DB_NAME=$MONGODB_DATABASE
      - DB_PORT=$MONGODB_DOCKER_PORT
      - CLIENT_ORIGIN=$CLIENT_ORIGIN
    networks:
      - backend
      - frontend

  bezkoder-ui:
    depends_on:
      - bezkoder-api
    build:
      context: ./bezkoder-ui
      args:
        - REACT_APP_API_BASE_URL=$CLIENT_API_BASE_URL
    ports:
      - $REACT_LOCAL_PORT:$REACT_DOCKER_PORT
    networks:
      - frontend  

volumes: 
  db:

networks:
  backend:
  frontend:
Enter fullscreen mode Exit fullscreen mode
  • mongodb:

    • image: official Docker image
    • restart: configure the restart policy
    • env_file: specify our .env path that we will create later
    • environment: provide setting using environment variables
    • ports: specify ports will be used
    • volumes: map volume folders
    • networks: join backend network
  • bezkoder-api:

    • depends_on: dependency order, mongodb service is started before bezkoder-api
    • build: configuration options that are applied at build time that we defined in the Dockerfile with relative path
    • environment: environmental variables that Node application uses
    • networks: join both backend and frontent networks
  • bezkoder-ui:

    • depends_on: start after bezkoder-api
    • build-args: add build arguments - environment variables accessible only during the build process
    • networks: join only frontent network

You should note that the host port (LOCAL_PORT) and the container port (DOCKER_PORT) is different. Networked service-to-service communication uses the container port, and the outside uses the host port.

Docker Compose Environment variables

In the service configuration, we used environmental variables defined inside the .env file. Now we start writing it.

.env

MONGODB_USER=root
MONGODB_PASSWORD=123456
MONGODB_DATABASE=bezkoder_db
MONGODB_LOCAL_PORT=7017
MONGODB_DOCKER_PORT=27017

NODE_LOCAL_PORT=6868
NODE_DOCKER_PORT=8080

CLIENT_ORIGIN=http://127.0.0.1:8888
CLIENT_API_BASE_URL=http://127.0.0.1:6868/api

REACT_LOCAL_PORT=8888
REACT_DOCKER_PORT=80
Enter fullscreen mode Exit fullscreen mode

Run MERN stack with Docker Compose

We can easily run the whole with only a single command:
docker-compose up

Docker will pull the MongoDB and Node.js images (if our machine does not have it before).

The services can be run on the background with command:
docker-compose up -d

$ docker-compose up -d
Creating network "react-node-mongodb_backend" with the default driver
Creating network "react-node-mongodb_frontend" with the default driver
Creating volume "react-node-mongodb_db" with default driver
Pulling mongodb (mongo:5.0.2)...
5.0.2: Pulling from library/mongo
16ec32c2132b: Pull complete
6335cf672677: Pull complete
cbc70ccc8ebe: Pull complete
0d1a3c6bd417: Pull complete
960f3b9b27d3: Pull complete
aff995a136b4: Pull complete
4249be7550a8: Pull complete
cc105ff5aa3c: Pull complete
82819807d07a: Pull complete
81447d2c233f: Pull complete
Digest: sha256:93ea50c5f15f9814870b3509449d327c5bc4d38f2b17c20acec528472811a723
Status: Downloaded newer image for mongo:5.0.2
Building bezkoder-api
Sending build context to Docker daemon  20.48kB
Step 1/6 : FROM node:14
 ---> 256d6360f157
Step 2/6 : WORKDIR /bezkoder-api
 ---> Running in 630b36161cfc
Removing intermediate container 630b36161cfc
 ---> cce099c5509c
Step 3/6 : COPY package.json .
 ---> 966883cd8e24
Step 4/6 : RUN npm install
 ---> Running in 246256e84187
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN node-express-mongodb@1.0.0 No repository field.

added 82 packages from 128 contributors and audited 82 packages in 8.017s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Removing intermediate container 246256e84187
 ---> bdab72a5d37b
Step 5/6 : COPY . .
 ---> e9069d0ee44a
Step 6/6 : CMD npm start
 ---> Running in 6f63286cae18
Removing intermediate container 6f63286cae18
 ---> 65452914f005
Successfully built 65452914f005
Successfully tagged react-node-mongodb_bezkoder-api:latest
WARNING: Image for service bezkoder-api was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building bezkoder-ui
Sending build context to Docker daemon  67.07kB
Step 1/12 : FROM node:14 as build-stage
 ---> 256d6360f157
Step 2/12 : WORKDIR /bezkoder-ui
 ---> Running in e135a434b996
Removing intermediate container e135a434b996
 ---> 80292facc18b
Step 3/12 : COPY package.json .
 ---> 595fbe352edd
Step 4/12 : RUN npm install
 ---> Running in 9d8ef80ff165

added 1661 packages from 793 contributors and audited 1666 packages in 115.944s

94 packages are looking for funding
  run `npm fund` for details

found 10 vulnerabilities (1 low, 5 moderate, 4 high)
  run `npm audit fix` to fix them, or `npm audit` for details
Removing intermediate container 9d8ef80ff165
 ---> 18a4f1382bc7
Step 5/12 : COPY . .
 ---> f99e2a8ef053
Step 6/12 : ARG REACT_APP_API_BASE_URL
 ---> Running in 47dca1457fb2
Removing intermediate container 47dca1457fb2
 ---> 8986f1482c8d
Step 7/12 : ENV REACT_APP_API_BASE_URL=$REACT_APP_API_BASE_URL
 ---> Running in 95687ba2d936
Removing intermediate container 95687ba2d936
 ---> 7a20778bca2b
Step 8/12 : RUN npm run build
 ---> Running in d0074ed04394

> react-crud@0.1.0 build /bezkoder-ui
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  52.78 KB  build/static/js/2.c9e8967b.chunk.js
  22.71 KB  build/static/css/2.fa6c921b.chunk.css
  2.39 KB   build/static/js/main.aae2fe51.chunk.js
  776 B     build/static/js/runtime-main.99b514f4.js
  144 B     build/static/css/main.9c6cdb86.chunk.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  bit.ly/CRA-deploy

Removing intermediate container d0074ed04394
 ---> 9cd424a1901b
Step 9/12 : FROM nginx:1.17.0-alpine
 ---> bfba26ca350c
Step 10/12 : COPY --from=build-stage /bezkoder-ui/build /usr/share/nginx/html
 ---> 2b4cbdbd908e
Step 11/12 : EXPOSE $REACT_DOCKER_PORT
 ---> Running in ced23b1795d6
Removing intermediate container ced23b1795d6
 ---> 2850341d70f0
Step 12/12 : CMD nginx -g 'daemon off;'
 ---> Running in d596e17eec46
Removing intermediate container d596e17eec46
 ---> fef41917c48b
Successfully built fef41917c48b
Successfully tagged react-node-mongodb_bezkoder-ui:latest
WARNING: Image for service bezkoder-ui was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating react-node-mongodb_mongodb_1 ... done
Creating react-node-mongodb_bezkoder-api_1 ... done
Creating react-node-mongodb_bezkoder-ui_1  ... done
Enter fullscreen mode Exit fullscreen mode

Now you can check the current working containers:

$ docker ps
CONTAINER ID   IMAGE                             COMMAND                  CREATED        STATUS         PORTS                                         NAMES
c9ee5ce3c370   react-node-mongodb_bezkoder-ui    "/bin/sh -c 'nginx -…"   Up 2 minutes   Up 2 minutes   0.0.0.0:8888->80/tcp, :::8888->80/tcp         react-node-mongodb_bezkoder-ui_1
f0c7d4174bdb   react-node-mongodb_bezkoder-api   "docker-entrypoint.s…"   Up 2 minutes   Up 2 minutes   0.0.0.0:6868->8080/tcp, :::6868->8080/tcp     react-node-mongodb_bezkoder-api_1
2f8390fc81dd   mongo:5.0.2                       "docker-entrypoint.s…"   Up 2 minutes   Up 2 minutes   0.0.0.0:7017->27017/tcp, :::7017->27017/tcp   react-node-mongodb_mongodb_1
Enter fullscreen mode Exit fullscreen mode

And Docker images:

$ docker images
REPOSITORY                        TAG      IMAGE ID       CREATED         SIZE
react-node-mongodb_bezkoder-ui    latest   fef41917c48b   3 minutes ago   22MB
react-node-mongodb_bezkoder-api   latest   65452914f005   6 minutes ago   961MB
mongo                             5.0.2    269b735e72cb   7 minutes ago   682MB
Enter fullscreen mode Exit fullscreen mode

Test the React UI:

docker-mern-nginx-example-test-ui

MongoDB Database:

docker-mern-nginx-example-test-database

And Node.js Express API:

docker-mern-nginx-example-test-api

Stop the Application

Stopping all the running containers is also simple with a single command:
docker-compose down

$ docker-compose down
Stopping react-node-mongodb_bezkoder-ui_1  ... done
Stopping react-node-mongodb_bezkoder-api_1 ... done
Stopping react-node-mongodb_mongodb_1      ... done
Removing react-node-mongodb_bezkoder-ui_1  ... done
Removing react-node-mongodb_bezkoder-api_1 ... done
Removing react-node-mongodb_mongodb_1      ... done
Removing network react-node-mongodb_backend
Removing network react-node-mongodb_frontend
Enter fullscreen mode Exit fullscreen mode

If you need to stop and remove all containers, networks, and all images used by any service in docker-compose.yml file, use the command:
docker-compose down --rmi all

Conclusion

Today we've successfully created MERN application with Docker and Nginx. Now we can deploy MERN stack: React + Nodejs Express and MongoDB on a very simple way: docker-compose.yml.

You can apply this way to one of following project:

Happy Learning! See you again.

Source Code

The source code for this tutorial can be found at Github.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .