Dockerizing a MERN Stack Application

Dockerizing a MERN Stack Application

·

7 min read

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

  1. 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 frontendfor 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 pagesdirectory, 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

  1. 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
  1. Initialize a new Node.js project by running the following command and following the prompts:
npm init -y
  1. Install the required dependencies for the backend.
npm install nodemon express helmet cors mongoose dotenv
  1. 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();
  1. 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}`);
});
  1. 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"
},
...
  1. 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.

  1. Go back to the root of your MERN application directory and create a new directory named nginx:
mkdir nginx
cd nginx
  1. 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

  1. Create a Dockerfile for the backend in the backenddirectory.
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"]
  1. Create a Dockerfile for the NGINX and the frontend in the nginxdirectory
# 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

  1. Create a db directory in the project root folder. Then create an init-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.

Did you find this article valuable?

Support Wynn’s blog by becoming a sponsor. Any amount is appreciated!