Back to all articles
DockerMERNDevOpsGitHub ActionsCI/CDVPS

Dockerizing a MERN Application and Deploying to a VPS with GitHub Actions

Build a complete deployment workflow for a MERN application with Docker, Nginx reverse proxy, and GitHub Actions CI/CD pipeline for automatic VPS deployment.

Dockerizing a MERN Application and Deploying to a VPS with GitHub Actions

Introduction

In this guide, we will build a complete deployment workflow for a MERN application where:

  • Frontend (React) is in a separate GitHub repository
  • Backend (Node.js/Express) is in a separate GitHub repository
  • MongoDB runs in Docker
  • Nginx acts as a reverse proxy
  • Deployment happens on a VPS
  • GitHub Actions automatically deploys on every push to the main branch

By the end, you'll have a production-ready CI/CD pipeline.


Final Architecture

Internet


Nginx (VPS)

 ┌──┴───────────┐
 │              │
 ▼              ▼
React App    Express API
(Container)  (Container)


            MongoDB
           (Container)

Project Structure

Frontend Repository

frontend/

├── src/
├── public/
├── package.json
├── Dockerfile
└── .github/
    └── workflows/
        └── deploy.yml

Backend Repository

backend/

├── src/
├── package.json
├── Dockerfile
├── docker-compose.yml
└── .github/
    └── workflows/
        └── deploy.yml

Step 1: Create VPS

Recommended Specs:

2 CPU
4 GB RAM
50 GB SSD
Ubuntu 24.04

Providers:

  • DigitalOcean
  • AWS EC2
  • Hetzner
  • Contabo
  • Hostinger VPS

Step 2: Install Docker

Update server:

sudo apt update
sudo apt upgrade -y

Install Docker:

curl -fsSL https://get.docker.com | sh

Verify:

docker --version

Step 3: Install Docker Compose

docker compose version

Ubuntu 24 usually includes Docker Compose automatically.


Step 4: Backend Dockerfile

Create:

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 5000

CMD ["npm","start"]

Build locally:

docker build -t crud-api .

Run:

docker run -p 5000:5000 crud-api

Step 5: Frontend Dockerfile

Create:

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

RUN npm install -g serve

EXPOSE 3000

CMD ["serve","-s","build","-l","3000"]

Build:

docker build -t crud-frontend .

Step 6: Backend Compose File

Create:

services:

  api:
    image: yourdockerhub/crud-api:latest
    container_name: crud-api

    ports:
      - "5000:5000"

    environment:
      MONGO_URI: mongodb://mongo:27017/cruddb

    depends_on:
      - mongo

  mongo:
    image: mongo:8

    container_name: mongo

    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

Step 7: Frontend Compose File

Create:

services:

  frontend:
    image: yourdockerhub/crud-frontend:latest

    container_name: crud-frontend

    ports:
      - "3000:3000"

Step 8: Create Docker Hub Account

Login:

docker login

Tag image:

docker tag crud-api yourdockerhub/crud-api:latest

Push:

docker push yourdockerhub/crud-api:latest

Same for frontend.


Step 9: Setup Nginx

Install:

sudo apt install nginx -y

Step 10: Backend Nginx Config

Create:

sudo nano /etc/nginx/sites-available/api
server {

    server_name api.example.com;

    location / {

        proxy_pass http://localhost:5000;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;
    }
}

Step 11: Frontend Nginx Config

server {

    server_name example.com;

    location / {

        proxy_pass http://localhost:3000;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;
    }
}

Enable:

sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/

sudo nginx -t

sudo systemctl restart nginx

Step 12: SSL Setup

Install Certbot:

sudo apt install certbot python3-certbot-nginx -y

Generate SSL:

sudo certbot --nginx

Select:

example.com
api.example.com

Done.


Step 13: VPS Directory Structure

/opt/apps/

├── backend
│   └── docker-compose.yml

└── frontend
    └── docker-compose.yml

Step 14: Create Deployment User

sudo adduser deploy

Add docker permission:

sudo usermod -aG docker deploy

Step 15: Generate SSH Key

On local machine:

ssh-keygen -t ed25519

Copy:

cat ~/.ssh/id_ed25519.pub

Paste into:

/home/deploy/.ssh/authorized_keys

Step 16: GitHub Secrets

Backend Repository:

VPS_HOST
VPS_USER
VPS_SSH_KEY

DOCKER_USERNAME
DOCKER_PASSWORD

Frontend Repository:

VPS_HOST
VPS_USER
VPS_SSH_KEY

DOCKER_USERNAME
DOCKER_PASSWORD

Step 17: Backend GitHub Action

Create:

.github/workflows/deploy.yml
name: Deploy Backend

on:
  push:
    branches:
      - main

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build Image
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/crud-api:latest .

      - name: Push Image
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/crud-api:latest

      - name: Deploy VPS

        uses: appleboy/ssh-action@v1.0.3

        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}

          script: |
            cd /opt/apps/backend

            docker compose pull

            docker compose down

            docker compose up -d

Step 18: Frontend GitHub Action

name: Deploy Frontend

on:
  push:
    branches:
      - main

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: docker/login-action@v3

        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/crud-frontend:latest .

      - name: Push
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/crud-frontend:latest

      - name: Deploy

        uses: appleboy/ssh-action@v1.0.3

        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}

          script: |
            cd /opt/apps/frontend

            docker compose pull

            docker compose down

            docker compose up -d

Deployment Flow

Developer pushes code:

git push origin main

GitHub Actions:

1. Checkout code
2. Build Docker image
3. Push image to Docker Hub
4. SSH into VPS
5. Pull latest image
6. Restart containers
7. Deployment complete

No manual server login required.


Useful Commands

View containers:

docker ps

View logs:

docker logs crud-api

Follow logs:

docker logs -f crud-api

Restart:

docker restart crud-api

Stop all:

docker compose down

Start all:

docker compose up -d

Production Improvements

For real-world production systems, add:

  • Multi-stage Docker builds
  • Health checks
  • Docker image versioning
  • Private Docker registry
  • Monitoring (Prometheus + Grafana)
  • Log aggregation
  • Automated database backups
  • Blue-Green deployment strategy
  • GitHub Environment approvals
  • Kubernetes (future scaling)

Conclusion

This setup provides:

  • Separate frontend and backend repositories
  • Dockerized applications
  • MongoDB in containers
  • Nginx reverse proxy
  • SSL certificates
  • GitHub Actions CI/CD
  • Automatic VPS deployments

This is a solid production-grade foundation for most startup and SaaS applications.

Blog | Durgesh Bachhav