Containerize and Orchestrate: Building a Real-Time(Three-tier) Chat App with Docker and Kubernetes

Photo by Growtika on Unsplash

Containerize and Orchestrate: Building a Real-Time(Three-tier) Chat App with Docker and Kubernetes

Hey DevOps Enthusiast! πŸš€

Ready to build your own full-stack chat application running in Docker containers and deployed using Kubernetes on your local machine? In this blog, we're going to walk through the process of containerizing a chat app, use Docker for containers, use Docker Compose for managing multiple containers, and finally deploy everything with Kubernetes (Kind) on your local machine.

I will explain everything in such a way that it will be understandable. Let's start!

What We’ll Build:

We’ll be building a real-time chat application that consists of:

  • Frontend: A React-based chat interface.

  • Backend: A Node.js server that handles chat messages and user authentication.

  • Database: MongoDB to store all the chat data and user profiles.

  • Real-Time Communication: Using Socket.io for instant messaging.

What You'll Need:

Before we begin, let's make sure you have everything set up:

  • Docker (to containerize the application)

  • Docker Compose (to run the application using multiple containers)

  • Kind (Kubernetes in Docker to run the application on a Kubernetes cluster)

  • Kubectl (Kubernetes CLI tool to control the cluster)

You can install all of the following tools from official documentation or instructions within the README files. The entire project, including the Docker and Kubernetes configurations, is available in the project repository on GitHub.


full-stack_chatApp
β”œβ”€β”€ backend/                           # Backend dir
β”‚   β”œβ”€β”€ Dockerfile                     # Dockerfile for the backend
β”‚   β”œβ”€β”€ package-lock.json              # Lock file for backend dependencies
β”‚   β”œβ”€β”€ package.json                   # Backend dependencies and scripts
β”œβ”€β”€ frontend/                          # Frontend application (React)
β”‚   β”œβ”€β”€ Dockerfile                     # Dockerfile for the frontend
β”‚   β”œβ”€β”€ index.html                     # Main HTML template for the React app
β”‚   β”œβ”€β”€ nginx.conf                     # Nginx config for frontend
β”‚   β”œβ”€β”€ package-lock.json              # Lock file for frontend dependencies
β”‚   β”œβ”€β”€ package.json                   # Frontend dependencies
β”œβ”€β”€ k8s/                               # Kubernetes manifests for deployment
β”‚   β”œβ”€β”€ README.md                      # Documentation for Kubernetes deployment
β”‚   β”œβ”€β”€ backend-deployment.yaml        # Kubernetes deployment for the backend
β”‚   β”œβ”€β”€ backend-service.yaml           # Kubernetes service for the backend
β”‚   β”œβ”€β”€ frontend-deployment.yaml       # Kubernetes deployment for the frontend
β”‚   β”œβ”€β”€ frontend-service.yaml          # Kubernetes service for the frontend
β”‚   β”œβ”€β”€ jwt-secret.yaml                # Kubernetes secret for JWT token
β”‚   β”œβ”€β”€ mongo-pvc.yaml                 # Persistent volume claim for MongoDB
β”‚   β”œβ”€β”€ mongodb-deployment.yaml        # Kubernetes deployment for MongoDB
β”‚   β”œβ”€β”€ mongodb-service.yaml           # Kubernetes service for MongoDB
β”‚   └── namespace.yaml                 # Kubernetes namespace for isolation
└── package.json                       # Project dependencies
β”œβ”€β”€ Jenkinsfile                        # Jenkins pipeline file
β”œβ”€β”€ README.md                          # Project's main documentation
β”œβ”€β”€ docker-compose.yml                 # Docker Compose configuration file

This structure will help you understand where each part of the application is located. The backend and frontend directories contain the application code along with the Dockerfile, while k8s contains Kubernetes deployment manifests. The docker-compose.yml file is used to manage all the containers together using Docker Compose.


Step 1: Build the Frontend Docker Image

Let's begin with the Frontend, which is a React app serving the user interface. It interacts with the backend for sending and receiving chat messages

Frontend Dockerfile

Here’s the Dockerfile for the frontend, which is responsible for building the app and serving it with Nginx:

# Stage 1: Build the app
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV NODE_ENV=production
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:stable-alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Explanation:

  1. Build Stage:

    • We use a Node.js image to install the app dependencies and build the React app for production.
  2. Serve Stage:

    • After building the app, we’ll use Nginx to serve the dist files.

Once the Dockerfile is ready, It’s time to build the frontend Docker image by running the following commands in your terminal:

cd frontend
docker build -t full-stack_frontend .

Step 2: Build the Backend Docker Image

Now, we require the Backend. The Backend is a Node.js server built with Express. This backend will handle all the API requests (such as login, sending messages, etc.) and real-time communication using Socket.io.

Backend Dockerfile

Here’s the Dockerfile for the backend:

# Stage 1: Build the backend
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .

# Stage 2: Run the backend in production
FROM node:18-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app ./
EXPOSE 5001
USER appuser
CMD ["npm", "start"]

Explanation:

  1. Build Stage:

    • We install only the production dependencies (npm ci --only=production) and copy the app files into the container.
  2. Production Stage:

    • Here we are setting up the container to run the Node.js server with Express. We are also adding a non-root user (appuser) for security.

To build the backend Docker image, run the following commands:

cd backend  
docker build -t full-stack_backend .

Step 3: Pull the MongoDB Docker Image

Our application requires a MongoDB database to store chat messages and user data. Luckily, MongoDB has an official Docker image, so we do not need to write a Dockerfile for it.

Pull the MongoDB image using the following command:

docker pull mongo:latest

Once we pull the image, we can run it as a container along with the frontend and backend.

Here's how we can run the Frontend, Backend, and MongoDB containers separately and test that everything is working fine:


Step 4: Run the MongoDB Container

Run the following command to start the MongoDB container:

docker run -d \
  --name mongo \
  -p 27017:27017 \
  --env MONGO_INITDB_ROOT_USERNAME=mongoadmin \
  --env MONGO_INITDB_ROOT_PASSWORD=secret \
  mongo:latest
  • -d: Runs the container in detached mode (background).

  • --name mongo: Names the container as mongo.

  • -p 27017:27017: Maps the MongoDB port to your local machine's port 27017.

  • --env: Sets environment variables for MongoDB to initialize with a root username and password.

To verify MongoDB is running correctly, you can use:

docker ps

MongoDB should now be accessible at localhost:27017.


Step 5: Create a Docker Network

Now we need to create a Docker Network.
Run the following command to create a docker network

docker network create fullstack-chatapp

Step 6: Run the Backend Container

Now, we need to run the Backend (Node.js with Express) to handle the chat API and real-time communication. The backend container must be linked to the MongoDB container to work properly.

Run the following command to start the backend:

docker run -d \
  --name backend \
  --network fullstack-chatapp \
  -p 5001:5001 \
  --env MONGODB_URI=mongodb://mongoadmin:secret@localhost:27017/chatdb?authSource=admin \
  --env JWT_SECRET=your_jwt_secret_key --> (experimental secret is provided in the Repo) \
  full-stack_backend:latest
  • --network fullstack-chatapp: This will enable the backend to connect with MongoDB using the fullstack-chatapp network.

  • --env MONGODB_URI: This will specify the MongoDB connection string, including username, password, and database.

  • --env JWT_SECRET: This will set the JWT secret key used to authenticate users.

To check if the backend is running correctly, use:

docker ps

You should see the backend running at localhost:5001.


Step 7: Run the Frontend Container

Now that the backend and database are up, we can run the Frontend (React app served by Nginx) to serve the chat interface.

Run the following command to start the frontend:

docker run -d \
  --name frontend \
  --network fullstack-chatapp \
  -p 80:80 \
  full-stack_frontend:latest
  • --network fullstack-chatapp: Ensures the frontend can connect to the backend via the fullstack-chatapp network.

  • -p 80:80: Maps the frontend to port 80 on your local machine.

To verify if the frontend is working:

docker ps

The frontend should be running and accessible at http://localhost.


Step 8: Verify the Application

At this point, all the individual services (Frontend, Backend, MongoDB) are running in separate containers. You can now verify if the chat application is working correctly:

  1. MongoDB:

    • MongoDB should be running on localhost:27017. You can access it via MongoDB clients like MongoDB Compass or by using mongo CLI to connect to it with the root credentials (mongoadmin / secret).
  2. Backend:

    • The backend API should be running on localhost:5001. To verify the backend, try sending a GET request to the backend using curl:

        curl http://localhost:5001/api/status
      
    • This should return some information about the API if it’s working correctly.

  3. Frontend:

    • Visit http://localhost in your browser. If everything is set up correctly, you should see the chat application frontend running.

    • Try logging in, sending messages, and verifying real-time communication (using Socket.io). If the frontend connects to the backend successfully, you should see live messages appearing.


Step 9: Shut Down the Containers

Once you’ve verified that everything is working, you can stop the containers using:

docker stop frontend backend mongo
docker rm frontend backend mongo

Step 10: Setting Up Docker Compose

Now that we have the Docker images ready for the frontend, backend, and MongoDB, we can use Docker Compose to run all the services together in one command.

Here’s a docker-compose.yml file to manage the services:

version: '3.8'
services:
  mongodb:
    image: mongo
    volumes:
      - mongo-data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=mongoadmin
      - MONGO_INITDB_ROOT_PASSWORD=secret
    ports:
      - "27017:27017"
    networks:
      - fullstack-chatapp

  backend:
    image: backend
    container_name: full-stack_backend
    environment:
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongoadmin:secret@mongodb:27017/dbname?authSource=admin
      - JWT_SECRET=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MzI2NTMxMDcsImV4cCI6MTc2NDE4OTEwNywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.qE3lxrcV9TM3nujRqJVeCHPd_4WFPO9eVZj8A_kjabI
      - PORT=5001
    depends_on:
      - mongodb
    networks:
      - fullstack-chatapp

  frontend:
    image: frontend
    container_name: full-stack_frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - fullstack-chatapp

networks:
  fullstack-chatapp:
    driver: bridge

volumes:
  mongo-data:

Explanation:

  • MongoDB: This is the MongoDB database container; it listens on port 27017. It leverages environment variables to get the root user and root password in place.

  • Backend: The backend connects to MongoDB on the URI we set in our MONGODB_URI environment variable, listening on port 5001.

  • Frontend: The frontend is served on port 80, and it relies on the backend.

Now, you can run the entire app using Docker Compose:

docker-compose up -d --build

Visit http://localhost in your browser to see the chat app live!


Step 11: Deploying with Kubernetes (Kind)

Appreciating you for sticking with the blog for this long journey! Now, let’s deploy this application using Kubernetes on your local machine. We'll be using Kind (Kubernetes in Docker) to create a lightweight Kubernetes cluster.

Step 11.1: Install Kind and Kubectl

Before we begin, make sure you have Kind and Kubectl installed. You can find installation instructions on their respective official websites as well in official Repo.

Once installed, you can verify the installation with:

kind version 
kubectl version --client

Step 11.2: Create a Kind Cluster

To create a Kubernetes cluster using Kind, run:

kind create cluster --name chat-app

This will set up a local Kubernetes cluster for you to deploy the app.

Step 11.3: Deploy the Application Using Kubernetes Manifests

Now, let’s use the Kubernetes manifests to deploy the app. These manifest files are YAML files that define how Kubernetes should run the app.

Here’s a breakdown of the Kubernetes steps:

  1. Create the Namespace:

     kubectl apply -f namespace.yaml
    
  2. Deploy MongoDB:

     kubectl apply -f mongodb-deployment.yaml
     kubectl apply -f mongodb-service.yaml
     kubectl apply -f mongo-pvc.yaml
    
  3. Deploy JWT Secret (for user authentication):

     kubectl apply -f jwt-secret.yaml
    
  4. Deploy the Backend:

     kubectl apply -f backend-deployment.yaml
     kubectl apply -f backend-service.yaml
    
  5. Deploy the Frontend:

     kubectl apply -f frontend-deployment.yaml
     kubectl apply -f frontend-service.yaml
    

Step 11.4: Access the Application

After deploying, you can check the status of the app using:

kubectl get pods -n chat-app

To access the frontend, use port forwarding:

kubectl port-forward service/frontend 8080:80 -n chat-app

Now, visit http://localhost:8080 in your browser to access the chat app.


Step 12: Clean Up the Kubernetes Cluster

When you’re done, you can delete the Kind cluster and clean up the resources:

kind delete cluster --name chat-app

Conclusion

By following this guide, you've learned how to set up Three Tier Application, ensuring your app is easy to manage and scale. Whether you’re just starting or already familiar with Docker and Kubernetes, this process gives you the foundation to build more complex applications.

Keep experimenting, and don't be afraid to tweak things to suit your needs. The more you practice, the more confident you’ll become with these powerful tools!

Happy Learning!

Β