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:
- React + Node.js + Express + MongoDB example: CRUD App
- React + Node.js Express + MongoDB: User Authentication with JWT example
- Integrate React with Node.js Express on same Server/Port
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:
Setup Nodejs App
You can read and get Github source code from one of following tutorials:
- Node.js, Express & MongoDb: Build a CRUD Rest Api example
- Node.js + MongoDB: User Authentication & Authorization with JWT
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",
...
}
}
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}.`);
});
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`
};
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
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
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 scriptnpm start
after the image is built.
Setup React App
You can read and get Github source code from one of following tutorials:
- React CRUD example to consume Web API
- React Typescript CRUD example to consume Web API
- React Redux CRUD App example with Rest API
- React Hooks CRUD example to consume Web API
- React Table example: CRUD App with react-table v7
- React Material UI examples with a CRUD Application
- React JWT Authentication & Authorization example
- React + Redux: JWT Authentication & Authorization example
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"
}
});
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;'
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
andENV
: get argument and set environment variable (prefixREACT_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:
-
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) andmongodb
(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:
-
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
: joinbackend
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 bothbackend
andfrontent
networks
-
-
bezkoder-ui:
-
depends_on
: start afterbezkoder-api
-
build-args
: add build arguments - environment variables accessible only during the build process -
networks
: join onlyfrontent
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
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
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
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
Test the React UI:
MongoDB Database:
And Node.js Express 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
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:
- React + Node.js + Express + MongoDB example: CRUD App
- React + Node.js Express + MongoDB: User Authentication with JWT example
Happy Learning! See you again.
Source Code
The source code for this tutorial can be found at Github.