Skip to main content

Command Palette

Search for a command to run...

Containerizing a MERN Stack Application with Docker

Updated
7 min read
Containerizing a MERN Stack Application with Docker
A

Associate DevOps Engineer Kubernetes | Docker | GCP/ AWS / Azure | Linux Web Development (HTML5, CSS3, Bootstrap, JS) Video Editing (DaVinci Resolve) Currently working on Terraform

Introduction

In the fast-paced world of software development, deploying applications seamlessly across different environments is crucial. This is where containerization comes into play. Containerization allows developers to package an application and its dependencies into a standardized unit known as a container. Among the many containerization tools available, Docker has emerged as a favorite, providing a robust and efficient solution for application deployment.

Docker simplifies the process of packaging, distributing, and running applications, ensuring consistency across various environments. Its popularity stems from its ability to streamline the deployment pipeline, enhance scalability, and facilitate collaboration among development teams.

Prerequisites

Before diving into the tutorial, let's ensure you have the necessary tools installed on your local machine. To follow along, you'll need:

  1. Docker: This is the core tool for containerization.

  2. Docker Compose: This tool simplifies the process of defining and running multi-container Docker applications.

With these prerequisites in place, you'll be ready to containerize your MERN stack application.

Project Structure

Understanding the structure of your MERN stack project is essential for a smooth containerization process. Typically, a MERN stack project consists of:

  • Client: This folder contains your React.js frontend code. It might have a structure similar to:

      mainApp/
      ├── client/
      │   ├── public/
      │   ├── src/
      │   │   ├── components/
      │   │   ├── App.js
      │   │   ├── index.js
      │   ├── package.json
      │   ├── package-lock.json
      │   ├── Dockerfile (for frontend)
    
  • Server: This folder contains your Node.js backend code. The structure might look like:

      mainApp/
      ├── server/
      │   ├── src/
      │   │   ├── routes/
      │   │   ├── models/
      │   │   ├── index.js
      │   ├── package.json
      │   ├── package-lock.json
      │   ├── Dockerfile (for backend)
    

In these examples, Dockerfile is a configuration file that defines how your Docker image should be built. It instructs Docker on what dependencies to install and how to set up your application within the container.

Understanding the organization of your project is fundamental before delving into the specifics of Dockerizing your MERN stack. In the upcoming sections, we'll explore writing Dockerfiles, creating Docker Compose configurations, and addressing common challenges in the containerization process.

Dockerfile for Node.js Backend

Writing the Dockerfile

Stage 1: Build Stage

Let's start crafting our Dockerfile for the Node.js backend. This two-stage Dockerfile optimizes the build process and results in a smaller production image.

# Stage 1: Build Stage
FROM node:14 AS build
WORKDIR /app

# Copying package files for leveraging Docker caching
COPY package*.json ./
RUN npm install

# Copying the entire application to the image
COPY . .

# Building the application
RUN npm run build

# Stage 2: Production Stage
FROM node:14-alpine
WORKDIR /app

# Copying only the necessary artifacts from the build stage
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json

# Setting environment variables
ENV NODE_ENV=production

# Exposing the necessary port
EXPOSE 3000

# CMD to run the application
CMD ["npm", "start"]

If you don't require build stage, you can also create a simple Dockerfile like -

# Use the official Node.js image
FROM node:14

# Set the working directory
WORKDIR /app

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

# Install dependencies
RUN npm install

# Copy the rest of the application code to the container
COPY . .

# Expose the port your Node.js app will run on
EXPOSE 3000

# Command to run your application
CMD ["node", "app.js"]

Building and Running the Backend Container

After writing our Dockerfile, let's build and run the Docker container for the Node.js backend.

# Build the Docker image for the backend
docker build -t backend-image ./path/to/your/backend

# Run the Docker container for the backend
docker run -d -p 3000:3000 --name backend-container backend-image

Potential Issues

  • Dependency Issues: Ensure all dependencies are correctly declared in the package.json file.

  • Port Conflict: Make sure the port specified in the Dockerfile and the run command is not in use.

Dockerfile for React.js Frontend

Writing the Dockerfile

Stage 1: Build Stage

Now, let's create the Dockerfile for the React.js frontend, following a similar two-stage approach.

# Stage 1: Build Stage
FROM node:14 AS build
WORKDIR /app

# Copying package files
COPY package*.json ./
RUN npm install

# Copying the entire application
COPY . .

# Building the application
RUN npm run build

# Stage 2: Production Stage
FROM node:14-alpine
WORKDIR /app

# Copying only the necessary artifacts
COPY --from=build /app/build /app/build

# Exposing the necessary port
EXPOSE 3000

# CMD to run the static build
CMD ["npm", "start"]

Building and Running the Frontend Container

Build and run the Docker container for the React.js frontend using the following commands.

# Build the Docker image for the frontend
docker build -t frontend-image ./path/to/your/frontend

# Run the Docker container for the frontend
docker run -d -p 80:3000 --name frontend-container frontend-image

Potential Issues

  • Build Failures: Check the build output for any errors during the npm install or npm run build stages.

  • Port Conflict: Ensure the specified port in the Dockerfile and the run command doesn't conflict with other processes.

Database Container

This is optional and one can directly use database image file from docker hub as used in this example.

To create a Dockerfile for running MongoDB as a Docker container, you can use the official MongoDB image from Docker Hub. Here's a basic example:

# Use the official MongoDB image
FROM mongo:latest

# Set the working directory (optional)
WORKDIR /data

# Expose MongoDB port
EXPOSE 27017

# Define the command to run when the container starts
CMD ["mongod"]

Explanation of the Dockerfile:

  • FROM mongo:latest: This line pulls the official MongoDB image from Docker Hub.

  • WORKDIR /data: This line sets the working directory inside the container (optional).

  • EXPOSE 27017: This line exposes the default MongoDB port.

  • CMD ["mongod"]: This line specifies the command to run when the container starts. In this case, it starts the MongoDB server.

Save this Dockerfile in a directory alongside your MongoDB configuration or initialization scripts if needed. Build the Docker image using:

docker build -t your-mongodb-image-name .

Replace your-mongodb-image-name with the desired name for your MongoDB Docker image.

Now, you can use this image to run MongoDB as a Docker container. For example:

docker run -d --name mongodb-container -p 27017:27017 your-mongodb-image-name

This command starts a MongoDB container named "mongodb-container" in detached mode, exposing port 27017 on the host. Adjust the container name and port mappings as needed.

Docker Compose

Define the services in docker-compose.yml.

Compose your services, including MongoDB, backend, and frontend, in a

" docker-compose.yml " file.

version: '3'
services:
  mongodb:
    image: mongo
    #adding environment is optional step
    environment:
      MONGO_INITDB_ROOT_USERNAME: your-username
      MONGO_INITDB_ROOT_PASSWORD: your-password
    ports:
      - "27017:27017"
    # You can check your network-name using 
    # "docker network ls" command and this is also an optional step
    networks:
      - your-network-name

  backend:
    build:
      context: ./path/to/your/backend
    ports:
      - "3000:3000"
    networks:
      - your-network-name
    depends_on:
      - mongodb

  frontend:
    build:
      context: ./path/to/your/frontend
    ports:
      - "80:3000"
    networks:
      - your-network-name
    depends_on:
      - backend

# optional step
networks:
  your-network-name:
    driver: bridge

Building and Running the Containers

Now, build and run the Docker containers using Docker Compose.

# Build and run the Docker containers using Docker Compose
docker-compose up -d

Managing Containers

Explore common Docker commands for managing containers.

# List all running containers
docker ps

# Retrieve logs from a container
docker logs container-name

# Run commands inside a running container
docker exec -it container-name sh

Potential Issues

  • Service Discovery: Ensure services start in the correct order using the depends_on option in Docker Compose.

  • Image Pull Failures: Check your internet connection and Docker Hub access for image pulls.

Common Errors and Troubleshooting

Containerization isn't always a smooth sail, and you might encounter a few bumps along the way. Let's address some common errors and their solutions:

Error: Unable to start container – Port already in use

  • Solution: Check if the port specified in your docker-compose.yml is already in use by another process on your machine. Choose a different port if needed.

Error: Image not found or service not recognized

  • Solution: Ensure your Docker images are built and tagged correctly. Verify the service names in your docker-compose.yml match the service names in your project.

Error: Application not responding

  • Solution: Check the logs using docker logs to identify any issues within the containers. Ensure all dependencies are correctly set up.

Conclusion

Congratulations! You've successfully containerized your MERN stack application using Docker. In this journey, we explored the basics of Docker Compose, learned essential commands for managing containers, and tackled common challenges that might arise during the containerization process.

This is just the beginning of your containerization adventure. As you continue to work with Docker, don't hesitate to explore advanced customization options and optimizations tailored to your project's unique requirements. The world of containers offers endless possibilities for creating scalable, consistent, and easily deployable applications. Happy coding!

More from this blog

D

DevToDevops

8 posts

At DevToDevOps, we are on a mission to empower individuals and teams in the ever-evolving landscape of DevOps. Our goal is to provide a platform where enthusiasts, beginners, and seasoned professional