In this tutorial, we will walk you through Dockerizing a production-ready MERN (MongoDB, Express, React, Node.js) application.
The MERN architecture is a popular stack used for building full-stack web applications. It is composed of four main technologies, namely MongoDB, Express.js, React, and Node.js. Each of these technologies plays a specific role in the stack, allowing developers to build efficient and scalable web applications.
Dockerization allows us to package our application into containers, ensuring consistency across different environments and simplifying deployment.
Step 1: Set Up the React Frontend
- Let’s create a new directory for your MERN application and navigate into it.
mkdir docker-mern
cd docker-mern
2. Create a new directory named frontend
for the front:
mkdir frontend
cd frontend
3. Initialize a new React project using Create React App command and install the dependencies.
npx create-react-app .
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-router-dom axios
4. Create a new folder called components/pages
the src folder. Inside the pages
directory, create a new file calledDashboard.js
Go to Material UI templates.
Click on the Dashboard source code, copy and paste the code into Dashboard.js
5. Remove the Chart.js, Deposit.js, and Orders.js components.
6. Modify the App.js
to include the routing.
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Dashboard from "./components/pages/Dashboard";
function App() {
return (
<Router>
<Routes>
<Route exact path="/" element={<Dashboard />} />
</Routes>
</Router>
);
}
export default App;
8. Run npm start
Step 2: Set Up the Node.js Backend
- Go back to the root of your MERN application directory and create a new directory named
backend
for the Node.js backend:
mkdir backend
cd backend
- Initialize a new Node.js project by running the following command and following the prompts:
npm init -y
- Install the required dependencies for the backend.
npm install nodemon express helmet cors mongoose dotenv
- Create a config directory, and inside the directory create a file named
db-connection.js
const mongoose = require("mongoose");
const DB_HOST = process.env.DB_HOST;
const DB_PORT = process.env.DB_PORT;
const DB_NAME = process.env.DB_NAME;
const DB_USER = process.env.DB_USER;
const DB_PASSWORD = process.env.DB_PASSWORD;
const DB_URL = `mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`;
async function connectToDatabase() {
try {
await mongoose.connect(DB_URL);
console.log('Connected to MongoDB successfully!');
} catch (error) {
console.error('Error connecting to MongoDB:', error);
}
}
connectToDatabase();
- Create a new file called
server.js
on the root folder of the backend. Copy and paste the code.
require("dotenv").config({
path: `./.env.${process.env.NODE_ENV}`,
});
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
// require("./config/db-connection"); comment out this first if no mongodb setup locally.
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
// disable 'X-Powered-By' header in response
app.disable("x-powered-by");
app.get('/api/welcome', function (req, res) {
res.send("Welcome to my page.");
})
// default case for unmatched routes
app.use(function (req, res) {
res.status(404);
});
const port = process.env.SERVER_PORT || 4000;
app.listen(port, () => {
console.log(`\nServer Started on ${port}`);
});
- Update
package.json
to add two more scripts.
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start-prod": "NODE_ENV=production node server.js",
"start": "NODE_ENV=development && nodemon server.js"
},
...
- Run
npm start
and test the API by accessing the URL http://localhost:4000/api/welcome
API
Step 4: Connecting the Back End to the Front End
Now we already set up the frontend and backend, we also tested both servers are working well.
Go back to thefrontend/src/components/pages/
folder, open Dashboard.js
and edit the code to call the API and display the response on the page.
...
import React, { useEffect, useState } from "react";
import axios from "axios";
export default function Dashboard() {
const [msg, setMsg] = useState("");
useEffect(() => {
const fetchMessage = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_API_SERVER}/api/welcome`);
setMsg(response.data);
} catch (error) {
console.error("Error fetching welcome message:", error);
setMsg("Error fetching welcome message.");
}
};
fetchMessage();
}, []);
return (
<ThemeProvider theme={defaultTheme}>
...
<Grid item xs={12} md={8} lg={9}>
<Paper
sx={{
p: 2,
display: "flex",
flexDirection: "column",
height: 240,
}}
>
{msg}
</Paper>
</Grid>
....
</ThemeProvider>
);
}
In the root folder of thefrontend
directory, create an environment variables file .env.development
. Do note that every variables name must start with the prefix “REACT_APP_
”
REACT_APP_API_SERVER=http://localhost:4000
Re-run the react project. You should see the output below.
Now create another environment variables file .env.production
We will use the NGINX proxy to route incoming requests.
REACT_APP_API_SERVER=http://localhost
Step 5: Set Up the Nginx proxy
NGINX can act as a reverse proxy to forward requests from clients to the appropriate backend server (Node.js server in this case). This separation allows you to manage frontend and backend services independently, making it easier to scale and update each part of the application.
And in a production environment, we do not run npm start
for React. Instead, we build the React application and serve it using a web server like NGINX.
- Go back to the root of your MERN application directory and create a new directory named
nginx
:
mkdir nginx
cd nginx
- Create a new file called
default.conf
on the root folder of nginx. Copy and paste the code.
upstream backend {
server backend:4000;
}
server {
listen 80;
location /api {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
error_log /var/log/nginx/error.log;
}
In this configuration, NGINX will listen on port 80 and serve the static files from the React build directory. The try_files
directive ensures that all incoming requests are handled by index.html
, allowing React Router to handle client-side routing.
Step 6: Create the Dockerfile
- Create a
Dockerfile
for the backend in thebackend
directory.
FROM node:18
WORKDIR /usr/src/app
RUN apt-get update
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4000
CMD ["npm", "run", "start-prod"]
- Create a
Dockerfile
for the NGINX and the frontend in thenginx
directory
# Stage 1: Build the React app
FROM node:18 AS build
WORKDIR /app
COPY ./frontend/package*.json ./
RUN npm install
COPY ./frontend/ ./
RUN npm run build
# Stage 2: Serve the React app using Nginx
FROM nginx:1.21
COPY --from=build /app/build /usr/share/nginx/html
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Step 7: Set Up the MongoDB user account
- Create a
db
directory in the project root folder. Then create aninit-mongo.js
file, copy and paste the code below to create a db user account.
db.createUser({
user: "dbadmin",
pwd: "P1ssw0rd",
roles: [
{
role: "readWrite",
db: "docker-mern-mongo",
},
],
});
2. Create a Dockerfile
for MongoDB.
# Use the official MongoDB image from Docker Hub
FROM mongo:latest
# Copy the init-mongo.js script to the Docker container
COPY ./init-mongo.js /docker-entrypoint-initdb.d/
# Set environment variables for MongoDB configuration
ENV MONGO_INITDB_ROOT_USERNAME=dbadmin
ENV MONGO_INITDB_ROOT_PASSWORD=P1ssw0rd
ENV MONGO_INITDB_DATABASE=docker-mern-mongo
EXPOSE 27018
# Start MongoDB with the entrypoint script from the base image
CMD ["mongod"]
Step 8: Create the docker-compose.yml
Create the docker-compose.yml
in the project root folder.
version: "3"
services:
mongo:
build:
context: ./db
container_name: docker-mern-mongo
ports:
- "27018:27017"
volumes:
- mongo_data:/data/db
backend:
build:
context: ./backend
container_name: docker-mern-backend
ports:
- "4000:80"
environment:
- NODE_ENV=production
depends_on:
- mongo
restart: always
nginx:
build:
context: .
dockerfile: ./nginx/Dockerfile
container_name: docker-mern-nginx
ports:
- "80:80"
depends_on:
- backend
restart: always
volumes:
mongo_data:
Uncomment the require(“./config/db-connection”);
in theserver.js
To start the MERN application in Docker containers, run the following command:
docker-compose up -d
The -d
flag runs the containers in detached mode, allowing you to continue using the terminal.
Docker containers
When open the browser, and enter the URL http://localhost you should be able to see the output below.
Open your browser and go to http://localhost/api/welcome
. You should see the "Welcome to my page" message.
Notice that we didn’t specify the port number on the URL, this is because we using the NGINX to route the request.
To stop and remove the containers, run:
docker-compose down
\Leave a comment if you need the full source code.*
By following the steps outlined in this tutorial, you can easily Dockerize a production-ready MERN application and take advantage of the benefits offered by each component of the MERN stack. Happy coding and building amazing web applications with MERN! 🚀
In our upcoming tutorial, we'll explore the process of integrating the sign-in and sign-up API into our React frontend.