Docker MERN stack example
Tags: webdev, javascript, docker, devops
Published: 2023-08-23 03:22:26

<a href="https://www.docker.com/">Docker</a> 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 <a href="https://docs.docker.com/compose/">Docker Compose</a> and <a href="https://www.nginx.com/">Nginx</a>.

Related Posts:
- <a href="https://www.bezkoder.com/react-node-express-mongodb-mern-stack/">React + Node.js + Express + MongoDB example: CRUD App</a>
- <a href="https://www.bezkoder.com/react-node-mongodb-auth/">React + Node.js Express + MongoDB: User Authentication with JWT example</a>
- <a href="https://www.bezkoder.com/integrate-react-express-same-server-port/">Integrate React with Node.js Express on same Server/Port</a>

<h2>MERN stack Application with Docker Overview</h2>
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:
<ul>
	<li>React for UI</li>
	<li>Node.js Express for API</li>
	<li>MongoDB for database</li>
</ul>

Docker Compose helps us setup the system more easily and efficiently than with only Docker. We're gonna following these steps:
<ul>
	<li>Setup Nodejs App working with MongoDB database.</li>
	<li>Create Dockerfile for Nodejs App.</li>
	<li>Setup React App.</li>
	<li>Create Dockerfile for React App.</li>
	<li>Write Docker Compose configurations in YAML file.</li>
	<li>Set Environment variables for Docker Compose</li>
	<li>Run the system.</li>
</ul>

Directory Structure:

<img src="https://www.bezkoder.com/wp-content/uploads/2021/05/docker-mern-nginx-example-structure.png" alt="docker-mern-nginx-example-structure" width="240" height="310" class="alignnone size-full wp-image-9181" />

<h2>Setup Nodejs App</h2>
You can read and get Github source code from one of following tutorials:
- <a href="https://www.bezkoder.com/node-express-mongodb-crud-rest-api/">Node.js, Express & MongoDb: Build a CRUD Rest Api example</a>
- <a href="https://www.bezkoder.com/node-js-mongodb-auth-jwt/">Node.js + MongoDB: User Authentication & Authorization with JWT</a>

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

Firstly, let's add <code>dotenv</code> module into <em>package.json</em>.

```json
{
  ...
  "dependencies": {
    "dotenv": "^10.0.0",
    ...
  }
}
```

Next we import <code>dotenv</code> in <em>server.js</em> and use <code>process.env</code> for setting up CORS and port.

```js
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}.`);
});
```

Then we change modify database configuration and initialization.

<strong>app</strong>/<strong>config</strong>/<em>db.config.js</em>
```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`
};
```

We also need to make a <em>.env</em> sample file that shows all necessary arguments.

<strong>bezkoder-api</strong>/<em>.env.sample</em>
```properties
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
```

<h2>Create Dockerfile for Nodejs App</h2>
Dockerfile defines a list of commands that Docker uses for setting up the Node.js application environment. So we put the file in <strong>bezkoder-api</strong> folder.

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

<strong>bezkoder-api</strong>/<em>Dockerfile</em>
```properties
FROM node:14

WORKDIR /bezkoder-api
COPY package.json .
RUN npm install
COPY . .
CMD npm start
```

Let me explain some points:
<ul>
	<li><code>FROM</code>: install the image of the Node.js version.</li>
	<li><code>WORKDIR</code>: path of the working directory.</li>
	<li><code>COPY</code>: copy <em>package.json</em> file to the container, then the second one copies all the files inside the project directory.</li>
	<li><code>RUN</code>: execute a command-line inside the container: <code>npm install</code> to install the dependencies in <em>package.json</em>.</li>
	<li><code>CMD</code>: run script <code>npm start</code> after the image is built.</li>
</ul>

<h2>Setup React App</h2>
You can read and get Github source code from one of following tutorials:
- <a href="https://bezkoder.com/react-crud-web-api/">React CRUD example to consume Web API</a>
- <a href="https://bezkoder.com/react-typescript-axios/">React Typescript CRUD example to consume Web API</a>
- <a href="https://bezkoder.com/react-redux-crud-example/">React Redux CRUD App example with Rest API</a>
- <a href="https://bezkoder.com/react-hooks-crud-axios-api/">React Hooks CRUD example to consume Web API</a>
- <a href="https://bezkoder.com/react-table-example-hooks-crud/">React Table example: CRUD App with react-table v7</a>
- <a href="https://bezkoder.com/react-material-ui-examples-crud/">React Material UI examples with a CRUD Application</a>
- <a href="https://bezkoder.com/react-jwt-auth/">React JWT Authentication & Authorization example</a>
- <a href="https://bezkoder.com/react-redux-jwt-auth/">React + Redux: JWT Authentication & Authorization example</a>

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

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

Then we open <em>http-common.js</em> for updating <code>baseURL</code> of axios instance with <code>process.env.REACT_APP_API_BASE_URL</code>.

```js
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"
  }
});
```

<h2>Create Dockerfile for React App</h2>
We're gonna deploy the React app behind an Nginx server.

Same as Nodejs, we put Dockerfile inside <strong>bezkoder-ui</strong> folder.

<strong>bezkoder-upi</strong>/<em>Dockerfile</em>
```properties
# 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;'
```

There are two stage:
- Stage 1: Build the React application
<ul>
	<li><code>FROM</code>: install the image of the Node.js version.</li>
	<li><code>WORKDIR</code>: path of the working directory.</li>
	<li><code>COPY</code>: copy <em>package.json</em> file to the container, then the second one copies all the files inside the project directory.</li>
	<li><code>RUN</code>: execute a command-line inside the container: <code>npm install</code> to install the dependencies in <em>package.json</em>.</li>
	<li><code>ARG</code> and <code>ENV</code>: get argument and set environment variable (prefix <code>REACT_APP_</code> is required).</li>
	<li>run script <code>npm run build</code> after the image is built, the product will be stored in <strong>build</strong> folder.</li>
</ul>

- Stage 2: Serve the React application with Nginx
<ul>
	<li>install the image of the <strong>nginx</strong> alpine version.</li>
	<li>copy the react build from Stage 1 into <code>/usr/share/nginx/html</code> folder.</li>
	<li>expose port (should be <code>80</code>) to the Docker host.</li>
	<li><code>daemon off;</code> directive tells Nginx to stay in the foreground.</li>
</ul>

<h2>Write Docker Compose for MERN application</h2>
On the root of the project directory, we're gonna create the <em>docker-compose.yml</em> file for the MERN stack. Follow <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">version 3</a> syntax defined by Docker:

```properties
version: '3.8'

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

volumes:

networks:
```

<ul>
	<li><code>version</code>: Docker Compose file format version will be used.</li>
	<li><code>services</code>: individual services in isolated containers. Our application has three services: <code>bezkoder-ui</code> (React), <code>bezkoder-api</code> (Nodejs) and <code>mongodb</code> (MongoDB database).</li>
	<li><code><a href="https://docs.docker.com/storage/volumes/">volumes</a></code>: named volumes that keeps our data alive after restart.</li>
	<li><code><a href="https://docs.docker.com/network/">networks</a></code>: facilitate communication between containers</li>
</ul>

Let's implement the details.

<em>docker-compose.yml</em>
```properties
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:
```

- <strong>mongodb</strong>:
<ul>
	<li><code>image</code>: official Docker image</li>
	<li><code>restart</code>: configure the <a href="https://docs.docker.com/config/containers/start-containers-automatically/#use-a-restart-policy">restart policy</a></li>
	<li><code>env_file</code>: specify our <em>.env</em> path that we will create later</li>
	<li><code>environment</code>: provide setting using environment variables</li>
	<li><code>ports</code>: specify ports will be used</li>
	<li><code>volumes</code>: map volume folders</li>
	<li><code>networks</code>: join <code>backend</code> network</li>
</ul>

- <strong>bezkoder-api</strong>:
<ul>
	<li><code><a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on">depends_on</a></code>: dependency order, <strong>mongodb</strong> service is started before <strong>bezkoder-api</strong></li>
	<li><code>build</code>: configuration options that are applied at build time that we defined in the <em>Dockerfile</em> with relative path</li>
	<li><code>environment</code>: environmental variables that Node application uses</li>
	<li><code>networks</code>: join both <code>backend</code> and <code>frontent</code> networks</li>
</ul>

- <strong>bezkoder-ui</strong>:
<ul>
	<li><code>depends_on</code>: start after <code>bezkoder-api</code></li>
	<li><code>build-args</code>: add build arguments - environment variables accessible only during the build process</li>
	<li><code>networks</code>: join only <code>frontent</code> network</li>
</ul>

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

<h2>Docker Compose Environment variables</h2>
In the service configuration, we used environmental variables defined inside the <em>.env</em> file. Now we start writing it.

<em>.env</em>
```properties
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
```

<h2>Run MERN stack with Docker Compose</h2>
We can easily run the whole with only a single command:
<code>docker-compose up</code>

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:
<code>docker-compose up -d</code>

```bash
$ 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
```

Now you can check the current working containers:

```bash
$ 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
```

And Docker images:

```bash
$ 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
```

Test the React UI:

<img src="https://www.bezkoder.com/wp-content/uploads/2021/05/docker-mern-nginx-example-test-ui.png" alt="docker-mern-nginx-example-test-ui" width="680" height="660" class="alignnone size-full wp-image-9182" />

MongoDB Database:

<img src="https://www.bezkoder.com/wp-content/uploads/2021/05/docker-mern-nginx-example-test-database.png" alt="docker-mern-nginx-example-test-database" width="580" height="390" class="alignnone size-full wp-image-9183" />

And Node.js Express API:

<img src="https://www.bezkoder.com/wp-content/uploads/2021/05/docker-mern-nginx-example-test-api.png" alt="docker-mern-nginx-example-test-api" width="513" height="331" class="alignnone size-full wp-image-9184" />

<h2>Stop the Application</h2>
Stopping all the running containers is also simple with a single command:
<code>docker-compose down</code>

```bash
$ 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
```

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

<h2>Conclusion</h2>
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: <em>docker-compose.yml</em>.

You can apply this way to one of following project:
- <a href="https://www.bezkoder.com/react-node-express-mongodb-mern-stack/">React + Node.js + Express + MongoDB example: CRUD App</a>
- <a href="https://www.bezkoder.com/react-node-mongodb-auth/">React + Node.js Express + MongoDB: User Authentication with JWT example</a>

Happy Learning! See you again.

<h2>Source Code</h2>
The source code for this tutorial can be found at <a href="https://github.com/bezkoder/docker-mern-nginx">Github</a>.