Deploy MERN stack in Docker using Docker compose

Deploy MERN stack in Docker using Docker compose

Docker is a containerization platform that uses OS-level virtualization to package software applications with their dependencies. It allows to build,test and deploy the application quickly.

Docker uses the concept of docker images and containers that can be multiple and need to be handled. In order to make efficient and quick utilization, it uses the concept of docker-compose.

MERN stack represents the full-stack application, stands for MongoDb(NoSQL) database, Express /Node JS for backend, RactJs/NextJs for frontend. We will use Next Js for frontend.

In this article, we'll explain how Docker compose works and helps in dockerization and containarization. And deployment of the MERN stack application using it.


We will cover:

  1. Docker Compose

  2. How Docker Compose works and it's benefits

  3. Deployment of MERN stack using Docker-compose


  1. Docker Compose

Docker Compose is a tool provided by Docker that allows you to define and run multi-container Docker applications. It uses a YAML file to configure the services, networks, and volumes required for your application's containers to communicate and work together. Lets' discuss the basic format and structure of docker-compose file.

version: '3'
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: my_database
    volumes:
      - db_data:/var/lib/mysql
volumes:
  db_data:

In this example:

  • version: '3' specifies the version of the Compose file syntax being used.

  • Under services, each service (container) is defined. In this case, there are two services: web and db.

  • For each service, you define the Docker image to use, any ports to expose, environment variables, and volumes to mount.

  • Under volumes, named volumes are defined. These are used to persist data between container restarts.

To use Docker Compose with this file, you would navigate to the directory containing the docker-compose.yml file and run commands such as docker-compose up to start the containers defined in the file, or docker-compose down to stop and remove the containers. There are many other commands and options available as well, which you can explore in the Docker Compose documentation.


  1. How docker-compse works and it's benefits

Docker Compose is a tool that simplifies the management of multi-container Docker applications. It works by allowing you to define your application's services, networks, and volumes in a single YAML file called docker-compose.yml. With this file, you can specify how your application's containers should be configured and how they should interact with each other.

Here's how Docker Compose works and its benefits:

  1. Service Definitions: In the docker-compose.yml file, you define the services that make up your application. Each service represents a container. You can specify which Docker image to use, the ports to expose, environment variables, volumes, networks, and other configuration options.

  2. Networking: Docker Compose automatically creates a network for your application, allowing containers to communicate with each other using service names as hostnames. This simplifies inter-container communication without the need to manually manage networking configurations.

  3. Volume Management: Docker Compose allows you to define volumes to persist data generated by your containers. These volumes can be shared among containers or mounted from the host machine, providing flexibility in managing data storage.

  4. Orchestration: Docker Compose can start, stop, and manage multiple containers simultaneously based on the configuration specified in the docker-compose.yml file. This simplifies the deployment and scaling of multi-container applications.

  5. Environment Management: Docker Compose allows you to set environment variables for your services, making it easy to customize the behavior of your containers based on different environments (e.g., development, testing, production).

  6. Dependency Management: Docker Compose can define dependencies between services, ensuring that dependent services are started before the services that rely on them. This helps in orchestrating complex application setups with multiple interconnected components.

Benefits of using Docker Compose include:

  • Simplicity: Docker Compose provides a simple and intuitive way to define, manage, and deploy multi-container applications using a single configuration file.

  • Portability: Docker Compose configurations are portable and can be shared across development environments, making it easy to maintain consistency between development, testing, and production environments.

  • Efficiency: Docker Compose automates the setup and management of multi-container applications, saving time and effort compared to manual configuration and management.

  • Scalability: Docker Compose can scale your application by adding additional instances of services or by distributing containers across multiple hosts using Docker Swarm or Kubernetes.


  1. Deployment of MERN stack using Docker-compose

Now, here we will create the MERN stack app, NextJs for frontend and Express Js for backend.

Make sure that you have installed the docker and docker compose and node js

docker --version
docker-compose --version
node --version
---------------------------------------------------------------
sudo apt install docker.io docker-compose nodejs

Folder structure


Let's setup the frontend first, create the latest

npx create-next-app@latest

create the .env for enviromental variables and Dockerfile for docker build .dockerignore for files that need to be ignored while building docker images

Dockerfile

# Use the official Node.js image as base
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /frontend

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install dependencies
RUN npm install 

# Copy the rest of the application code
COPY . .

RUN npm run build
COPY .next ./.next

# Expose port 3000
EXPOSE 3000

# Command to run the application
CMD ["npm", "run", "dev"]

.dockerignore

node_modules
.gitignore
.dist
.env

Similarly, setup for the backend express js

npm init
touch index.js
npm install express dotenv mongoose

package.json

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "sahadev dahit",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.18.2",
    "mongoose": "^8.2.0"
  }
}

after creating index.js let's use the following code

const express = require("express");
const mongoose = require('mongoose');

require("dotenv").config();

const app = express();
const port = process.env.PORT || 4000; // Use process.env.PORT or default to 4000
const secret_one = process.env.secret_one;
const secret_two = process.env.secret_two;
const secret_three = process.env.secret_three;

console.log("mongo url  "+process.env.MONGO_URL);
const dbConnect = async () => {
    try {
      await  mongoose.set("strictQuery", false);
        const connected = await mongoose.connect(process.env.MONGO_URL);
        console.log(`Mongodb connected ${connected.connection.host}`);
    } catch (error) {
        console.log(`Error: ${error.message}`);
        process.exit(1);
    }
};

dbConnect()
// Define a route handler for the root path
app.get("/", (req, res) => {
  res.send("Hello World! Welcome to docekrization & contanarization");
});

// List all the secret
app.get("/env", (req, res) => {
  res.status(200).json({
    first: secret_one,
    second: secret_two,
    third: secret_three,
  });
});
// Start the server
app.listen(port, () => {
  console.log(`Server is listening at http://localhost:${port}`);
});

inside backend .env

MONGO_URL='mongodb://root:secret@mongo:27017/products?authSource=admin'
PORT=4000
secret_one="From secret One"
secret_two="From secret Two"
secret_three="From secret Three"

Now create the .dockerignore

node_modules
.gitignore
.dist
.env

And also create the Dockerfile

# Use the official Node.js image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /backend

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY . .

# Expose port 4000
EXPOSE 4000

# Command to run the application
CMD ["node", "index.js"]

Now setup the docker-compose.yaml

create docker-compose.yml

version: '3'

services:
  mongo:
    container_name: mongodb
    image: mongo:latest
    restart: always
    ports:
      - "27017:27017"
    env_file:
      - ./.env
    volumes:
     - "/mongo-data"
    networks:
      - app_network

  backend:
    container_name: backend_app
    restart: always
    build:
      context: ./backend
      dockerfile: Dockerfile
    env_file:
      - ./backend/.env
    ports:
      - "4000:4000"
    networks:
      - app_network
    depends_on:
      - mongo

  frontend:
    container_name: frontend_app
    restart: always
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    networks:
      - app_network

networks:
  app_network:
    driver: bridge

also .env file which contains the enviroment variables for docker-compose

MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=secret
MONGO_INITDB_DATABASE=products

This Docker Compose file defines a multi-container application consisting of a MongoDB database (`mongo` service), a backend application (`backend` service), and a frontend application (`frontend` service).

Let's break down the Docker Compose file:

  1. MongoDB Service (mongo):

    • This part defines a MongoDB database.

    • It specifies the image to use for MongoDB, which is the latest version available.

    • MongoDB listens on port 27017 inside the container, which is mapped to port 27017 on the host machine.

    • It loads environment variables from a .env file.

    • It creates a volume named /mongo-data to persist MongoDB data.

    • The service is connected to a custom network named app_network.

  2. Backend Service (backend):

    • This part defines a backend application.

    • It specifies the build context for the backend code and loads environment variables from a .env file.

    • The backend application listens on port 4000 inside the container, mapped to port 4000 on the host machine.

    • It depends on the MongoDB service to ensure the database is ready before starting.

    • The service is connected to the same app_network as MongoDB.

  3. Frontend Service (frontend):

    • This part defines a frontend application.

    • It specifies the build context for the frontend code.

    • The frontend application listens on port 3000 inside the container, mapped to port 3000 on the host machine.

    • It is also connected to the app_network.

  4. Networks:

    • A custom network named app_network is defined to allow communication between containers.
  5. Environment Variables for MongoDB Initialization:

    • Environment variables are set for initializing the MongoDB database with a root username, password, and initial database name.

In summary, this Docker Compose file sets up a development environment with MongoDB, a backend server, and a frontend server, all connected on a custom network. It ensures proper initialization order, where the backend depends on the availability of MongoDB.


sudo docker-compose up -d 
# after sucessful sun
sudo docker images
sudo docker ps

now make sure that the mongodb and backend is running

sudo docker logs <container_id>

Here, mongodb and backend service is running properly. And in docker-compose file we are creating the mongo from dockerhub and backend service depends on the mongo service before initialization.

Now check the frontend and backend in their respective port

In this way, we can deploy three-tier full-stack MERN Stack application in Docker using the docker-compose in an efficient way.

Thanks for reading.............................................