Are you searching for a way to make your MERN application compatible with all computers while developing the application? You’ve come to the right place! Dockerize your application, and this MERN stack tutorial is just what you need.
In this tutorial, you’ll learn the basics of Dockerizing a MERN stack, how it works, and how to implement it in various applications.
Read on and solve compatibility problems by Dockerizing your application!
Prerequisites
This tutorial will be a hands-on demonstration. If you’d like to follow along, be sure you have:
- An operating system supported by Docker – This tutorial uses Windows 10.
- Virtualization enabled and Linux kernel installed (for windows).
- API testing service (Postman).
Setting up Docker
Docker is a software platform that fastens the building, testing, deploying, and managing applications. Docker uses a container to store all dependencies and operating system configurations necessary for applications to run in any environment.
Before Dockerizing a MERN stack, you’ll first need to create a custom node image using a Docker file.
1. Open the Docker application to check if Docker started successfully. Below, you can tell the Docker engine is running since the status bar (bottom-left) is green.
2. Next, create a project folder named MERN-Docker. This folder will hold all resources for this tutorial.
3. Create a .js file with your preferred code editor in your project folder (MERN-Docker). You can name the file as you like, but the file is named server.js in this tutorial. The server.js file will contain all codes for the node application.
4. Open your terminal and run the following npm command to initialize the application (init –y) and create a package.json file.
npm init --y
5. Finally, run the below command to install the express
dependency, allowing you to create APIs for your node application.
npm i express
Setting up an Express Server
After initializing the MERN application and installing the express dependency, you’ll set up a simple express server. This tutorial uses a simple express server to demonstrate how Express and Node applications can be dockerized.
1. Open the server.js file and create an express GET
route to /
.
The code below sends a welcome message when a GET request is sent to http://localhost:5000.
//Importing and creating an instance of express
const express = require("express");
const app = express();
//Setting PORT to 5000 if PORT is not listed in environmental variables.
const PORT = process.env.PORT || 5000;
// Creating the `GET` route
app.get("/", (req, res) => {
res.send("<h2>Welcome Friends</h2>");
});
//Starting the express server
app.listen(PORT, () =>
console.log(`Server running at http://localhost:${PORT}`)
);
2. Now, run the node command below to start the express application.
node server.js
If the application is running correctly, you’ll see the output below.
3. Finally, make a GET request to http://localhost:5000 using an API testing service, like Postman, to test the express route.
Creating and Building a Custom Image in this Mern Stack Tutorial
After setting up the route, the next step is to integrate the express application into a Docker container using a base image. In this tutorial, you’ll use the node’s official base image to set up the container.
1. Create a file named Dockerfile and populate the following code, which creates a custom node image for your application. A customized image allows you to add your source code to the image and the configurations for your image.
# Sets the base image of the application to the node’s official image.
FROM node:17
# Sets the Working Directory as "/server"
WORKDIR /server
# Copies the package.json file into "/server" and runs npm i
COPY package.json /server
RUN npm i
# Copies the entire source code into "/server"
COPY . /server
# Specifies the port the node app will be running on
EXPOSE 5000
# Runs "node server.js" after the above step is completed
CMD ["node", "server.js"]
2. Run the docker build command below to create a customized image called (-t) node-image in the working directory (.).
docker build -t node-image .
The output below shows how Docker uses the Dockerfile to build the image.
3. Lastly, run the docker image command below to list (ls) all built images.
docker image ls
Below, you can see all available images, including the node image you created.
Setting up nodemon
You’ve successfully built a custom node image, and that’s great. But you’ll need help to develop your application when there are changes, and that’s where adding nodemon comes in.
nodemon automatically restarts the application when file changes are detected. But first, you’ll have to install it.
1. Run the npm
command below to install nodemon
as a dev dependency (--save-dev
).
npm i nodemon --save-dev
2. Next, open the package.json file in your preferred code editor to add the configuration below.
"scripts": {
"start": "node server.js",
"dev": "nodemon -L server.js"
},
3. Open the Dockerfile and change the CMD
command to the one below.
CMD ["npm", "run", "dev"]
4. Lastly, run the below docker build
command to rebuild the image (node-image
).
docker build -t node-image .
You can see below that Docker only loaded the cached data in the second step and ran the command from the third to the last step. This behavior results from the changes you made to the package.json file (Docker caching).
Speeding up Docker Creations by Excluding Files and Folders
Perhaps you want to speed up the Docker creation and protect sensitive content. If so, create a .dockerignore file where you’ll specify the files and folders to ignore from being copied into your Docker container.
1. Create a file named .dockerignore with your text editor and list the files, as shown below, to exclude from copying into your Docker container.
node_modules
Dockerfile
.dockerignore
2. Now, run the following docker build
command to rebuild the image (node-image
).
docker build -t node-image .
3. After building the image, execute the docker run command below to start a node-app container using the node-image on port 5000 (-p 5000:5000). This command also makes the container accessible through http://localhost:5000.
By default, Docker has a security mechanism that prevents other machines from accessing the Docker container. The only way to access the Docker container is by specifying an access port.
docker run --name node-app -d -p 5000:5000 node-image
To stop the container, run docker rm node-app -f.
4. Now, run the docker ps command below to view all active containers (-a).
docker ps -a
Below, you can see your Docker container (node-app) is active.
5. Run the docker exec command below to start an interactive (-it) shell (bash) inside the node-app container.
docker exec -it node-app bash
6. Finally, run the below dir
command to check if the files in the .dockerignore file were added to the container (node-app
).
dir
You can tell in the output below that the files you listed in the .dockerignore file are not included in the note-app Docker container.
The node_modules folder in the container was generated by the npm i command from the Dockerfile.
Creating the node-image with Docker-compose
You’ve learned a container’s basic life cycle: building, starting, and stopping a container. But can the lifecycle still be improved? Docker-compose lets you simplify the lifecycle of not just one but numerous containers.
With Docker-compose, you only need to run one Docker command to start up all containers and one command to shut down all containers instead of running a series of commands. These Docker commands follow the instructions listed in the docker-compose file.
To get started with Docker-compose, you need a YAML file that will contain all services and the configurations for these services. Some of these configurations include.
- Build Configuration: This contains the location of the Dockerfile you want to use to build the service and other build options.
- Images: You can use images in hub.docker.com instead of building your custom image.
- Environment variables: It stores configurations to variables in your code.
- Ports: This option specifies what port the application will run on.
- Network: This option enables one container to communicate with another container.
Create a YAML file named docker-compose.yml file and populate the configuration below, which creates a custom node image.
YAML is indentation sensitive, so ensure you use the appropriate indexing.
# Version of Docker-compose
version: '3.8'
services:
# Service name
node:
# Creating a custom image
build:
# Location to the Dockerfile
context: .
# Name of the Dockerfile
dockerfile: Dockerfile
ports:
# External port:Internal port
- 5000:5000
Run the command below to build up and start the node service.
docker-compose up --build
Configuring Volumes for Persisting Data in Docker Container
After creating the node image, you’ll need to store static data and sync your source code to the source code in the container. How? By configuring volumes and bind mounts for Docker container.
In this tutorial, you’ll start configuring volumes first. Volumes in Docker are directories outside the Docker container that contain the data for that container. Volumes are primarily used to store persisting data, such as source codes, log files, etc.
1. Open your docker-compose file in your code editor.
2. Add the volume configurations below under the node
service of the docker-compose file.
The configuration below creates a volume named nodeVolume and stores the volume in a folder named server. But feel free to change the name of the volume and the folder.
---
node:
---
# ADD THE CONFIGURATION FROM THIS POINT to create a volume named nodeVolume
volumes:
# Syntax <nameOfVolume>:<directorInDocker>
- nodeVolume:/server
# Making the node service volume accessible to other services.
volumes:
# Declaring the node service volume.
nodeVolume:
3. Lastly, run the command below to rebuild the image (node-image).
docker-compose up -d --build
Configuring Bind Mount to Sync Local Directory to Docker Container
Bind mount is a mechanism that syncs a folder in your local machine to a folder in the Docker container. A bind mount stores data in the container, but the data goes too when the container gets deleted.
Bind mounts are primarily used in the development stage where data are dynamic (information frequently changes). With a bind mount, you don’t have to rebuild the application every time a change is made to the application’s source code.
To configure a bind mount:
1. Open your docker-compose file and add the code below in the node
service under volumes
.
The code below syncs the working directory for the application to the /app directory in the container. At the same time, the code prevents your source code from making changes to the node_modules file in the app directory.
---
node:
volumes:
---
# ADD THE CONFIGURATION FROM THIS POINT to sync the working directory
# for the application to the /app directory in the container
- .:/server
- /server/node_modules
2. Run the command below to rebuild the node image.
docker-compose up -d --build
Modifying files from the /app directory of the application in Docker will affect the files in your local machine since the folders are synced. To restrict Docker from making changes to your application’s source code, add the read-only option (:ro) to your bind mount config, as shown below.
node:
volumes:
- ./:./server:ro # Adding the read-only option
- - /server/node_modules
3. Open the server.js file in your code editor, replace h1
header to h5
, as demonstrated below, and save the changes. These changes in the server.js file lets you test if the bind mount config works.
app.get("/", (req, res) => {
res.send("<h5>Welcome Friends</h5>");
});
4. Now, run the command below to view all running containers.
docker ps
5. Now, run the docker exec
command below to run an interactive shell (-it
) of your running node container (mern-docker_node_1
).
docker exec -it mern-docker_node_1 bash
6. Finally, run the cat
command below to display the changed content inside the server.js
file in your node container.
cat server.js
As you can see below, the header changed to h5.
Connecting MongoDB to the MERN Application
MongoDB is a NoSQL, free, open-source, cross-platform document-oriented database program. In this tutorial, you’ll set up MongoDB and see how the node service from the previous section can communicate with MongoDB.
Open the docker-compose file on your code editor, and add the configuration specified below under the node
service.
This configuration uses Mongo’s official Docker image to build the MongoDB service (container).
version: '3.8'
services:
node:
...
# ADD THE CONFIGURATION FROM THIS POINT to build the MongoDB service
environment:
- PORT=5000
# For security, specify a username and password as environmental variables
# Username for the mongo database
- MONGO_INITDB_ROOT_USERNAME=mern
# Password for the mongo database
- MONGO_INITDB_ROOT_PASSWORD=merndocker
# Enables the mongo service to start before the node service
depends_on:
- mongo
# Name of mongo service
mongo:
# Official mongo image from docker.hub
image: mongo
environment:
# Username for the mongo database
- MONGO_INITDB_ROOT_USERNAME=mern
# Password for the mongo database
- MONGO_INITDB_ROOT_PASSWORD=merndocker
volumes:
# <nameOfVolume>:<directorInDocker>
- mongoDB:/data/db
volumes:
# Making the volume accessible by other containers
mongoDB:
Now, run the docker-compose
command below to --build
and start the mongo service.
docker-compose up -d --build
As you can see below, Docker is creating a volume for the mongo service.
Linking NodeJS to MongoDB
After building the mongo service, you can now link the NodeJS service to MongoDB. Linking Nodejs to MongoDB enables you to store data in the MongoDB database.
Using the name of a service is one of the common ways to communicate with different containers. And this tutorial uses the mongoose dependency to Link the node service to MongoDB. But you’ll first have to install mongoose.
1. Run the command below to install mongoose
.
npm i mongoose
2. Next, open the server.js file and add the code below, which imports the mongoose dependency and uses it to link NodeJS with MongoDB.
The code below uses the username and password you stored as environment variables in the docker-compose file to connect the node service with MongoDB.
const mongoose = require('mongoose');
// Gets the Username and Password
const MONGO_URI = `mongodb://${process.env.MONGO_INITDB_ROOT_USERNAME}:${process.env.MONGO_INITDB_ROOT_PASSWORD}@mongo:27017`;
// Creating the connect function
const connectDB = async () => {
await mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("Mongo connected successfully"))// Logs out successful when MongoDB connects.
.catch((e) => {
console.log(e.message);// Logs out the error message if it encounters any.
});
};
// Calling the Connect Function
connectDB();
...
3. Now, run the docker-compose command below to rebuild the node image.
docker-compose up -d --build
4. Finally, run the following command to open the logs for the application and check if MongoDB connected successfully.
docker-compose logs
Below, you can see MongoDB connected successfully.
Adding Redis to the MERN Application
You’ve just added MongoDB as a second service to the MERN application, and now you’ll be adding Redis as the third. Redis is a NoSQL database commonly used to store cached data and tokens.
Open the docker-compose file, and add the following configuration below the mongo
service under services
, as shown below.
This configuration sets up Redis’ official Docker image
services:
---
mongo:
---
# ADD THE CONFIGURATION FROM THIS POINT to set up the Redis service
redis:
image: redis
Setting up the Client-side of the MERN application
From setting up your application on the server side, delve into setting up a React app as the client-side of the MERN application. React is a JavaScript library for building user interfaces.
1. Run the command below to create a simple React application. This command automatically creates a directory named client in the root directory of the MERN application.
npx create-react-app client
The create-react-app command starts installing all the required dependencies on the output below.
2. Once installation completes, open the client directory in the root directory of the MERN application, and create a Dockerfile inside it. You can name the Dockerfile differently, but the Dockerfile is named react.dockerfile in this tutorial.
3. Add the code below into the Dockerfile (react.dockerfile), which creates a custom React image.
# Official node image
FROM node:17
# Setting the working directory to "/client"
WORKDIR /client
# Copies the package.json file into "/client" and run npm i
COPY package.json /client
RUN npm install
# Copies the entire react source code into "/client"
COPY . /client
EXPOSE 3000
# Starting the react app
CMD [ "npm", "start"]
4. Now, open your docker-compose file and replace the content with the code below.
The following code adds a react service under services with the Dockerfile’s location and Dockerfile’s name.
You’ll add portions of the code you’ll add in the docker-compose file, and you’ll see the entire code in the last part of these steps.
# Version of Docker-compose
version: '3.8'
services:
# Add the react service
react:
# Location to the dockerfile
context: ./client
# Name of the dockerfile
dockerfile: react.dockerfile
5. Configure the volumes
, ports
, and depends_on
options, as demonstrated below.
volumes:
# Bind-mounts configuration
- ./client:/client
# Ignoring any changes made in "node_modules" folder
- /client/node_modules
ports:
# External port:Internal port
- 3000:3000
depends_on:
# Starts up the node service before starting up the react service
- node
6. Finally, add the configuration below to add an environment variable to enable hot reload in the Docker container. Hot reload refreshes a react page and re-renders its components.
environment:
# Enabling hot reload
- CHOKIDAR_USEPOLLING=true
Following the steps above will bring you to the configuration below.
version: '3.8'
services:
react:
build:
context: ./client
dockerfile: react.dockerfile
volumes:
- ./client:/client
- /client/node_modules
ports:
- 3000:3000
environment:
- CHOKIDAR_USEPOLLING=true
depends_on:
- node
node:
---
mongo:
---
redis:
---
Conclusion
This tutorial aimed to teach you how to set up your application using Docker and make it compatible with other devices. Do you feel that’s the case? At this point, you’ve learned the basics of bosting your MERN stack application’s development.
As a next step, why not learn to use NGINX to set up a proxy for your application and deploy it to Docker?