Sandesh Shrestha
Sandesh Shrestha
Jan 2025 – Present/Advanced
LockCodebase is confidential

Case Study: Multi-Environment DevOps Automation Project

Project at a Glance

  • Role: DevOps Engineer & Infrastructure
  • Duration: Jan 2025 – Present
  • Stack: Docker, GitHub Actions, PM2, NGINX, Node.js, Bash, Linux (Ubuntu), DigitalOcean VPS
  • Team Size: 1 (Full ownership)
  • Deployment Targets: Development, Staging, Production environments
  • Infrastructure: Multi-environment containerization with automated deployments
  • Domain Management: Automated domain mapping and SSL certificate management

My Role & Impact

  • Multi-Environment Architect: Designed and implemented containerized applications across development, staging, and production environments using Docker.
  • DevOps Automation: Built comprehensive CI/CD pipelines with GitHub Actions, enabling automated deployments from code push to production.
  • Domain & Server Management: Automated domain mapping, NGINX configuration, and SSL certificate management across multiple environments.
  • Process Orchestration: Implemented PM2 for application lifecycle management with clustering, monitoring, and automatic restarts.
  • Infrastructure Automation: Created reusable deployment scripts for automated server updates, builds, and service restarts.
  • Server Configuration: Managed NGINX reverse proxy setup with sites-available/sites-enabled configuration and symbolic links.

Highlights

Multi-Environment Docker Containerization

  • Separate Docker configurations for development, staging, and production environments.
  • Environment-specific Docker Compose files with different configurations.
  • Automated container builds and deployments across all environments.
  • Volume mounting and port mapping for different deployment scenarios.

Automated CI/CD Pipeline

  • GitHub Actions workflows triggered on code pushes to different branches.
  • Automated testing, building, and deployment to appropriate environments.
  • Environment-specific environment variables and configurations.
  • Automated rollback capabilities in case of deployment failures.

Domain & Server Management

  • Automated domain mapping to different server ports and applications.
  • NGINX configuration with sites-available and sites-enabled setup.
  • Symbolic link creation for domain routing and load balancing.
  • SSL certificate automation and renewal across all domains.

Process Management & Monitoring

  • PM2 cluster mode for high availability and load distribution.
  • Application monitoring, logging, and automatic restart capabilities.
  • Memory and CPU monitoring with automatic scaling.
  • Health checks and status monitoring across all environments.

Infrastructure Automation

  • Reusable deployment scripts for server updates and application restarts.
  • Automated git pull, build, and PM2 restart workflows.
  • Environment-specific configuration management.
  • Backup and recovery automation scripts.

System Architecture

Multi-Environment CI/CD Pipeline

GitHub Actions Workflow

name: Multi-Environment Deployment

on:
  push:
    branches: [main, develop, staging]

jobs:
  # Development Deployment
  deploy-development:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Deploy to Development Server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.DEV_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app-dev
            git pull origin develop
            npm install
            npm run build
            pm2 restart app-dev

  # Staging Deployment
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/staging'
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Deploy to Staging Server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app-staging
            git pull origin staging
            npm install
            npm run build
            pm2 restart app-staging

  # Production Deployment
  deploy-production:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Deploy to Production Server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app
            git pull origin main
            npm install
            npm run build
            pm2 restart app

      - name: Notify Deployment
        uses: 8398a7/action-slack@v3
        with:
          status: success
          channel: "#deployments"
          text: "Production deployment successful!"

Multi-Environment Docker Configuration

Development Environment

# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3001

CMD ["npm", "run", "dev"]
# docker-compose.dev.yml
version: "3.8"
services:
  app-dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=development
      - PORT=3001
    volumes:
      - .:/app
      - /app/node_modules
    restart: unless-stopped

Staging Environment

# Dockerfile.staging
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3002

CMD ["npm", "start"]
# docker-compose.staging.yml
version: "3.8"
services:
  app-staging:
    build:
      context: .
      dockerfile: Dockerfile.staging
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=staging
      - PORT=3002
    restart: unless-stopped

Production Environment

# Dockerfile.prod
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]
# docker-compose.prod.yml
version: "3.8"
services:
  app-prod:
    build:
      context: .
      dockerfile: Dockerfile.prod
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
    restart: unless-stopped

NGINX Configuration & Domain Management

Sites-Available Configuration

# /etc/nginx/sites-available/dev.yourdomain.com
server {
    listen 80;
    server_name dev.yourdomain.com;

    location / {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# /etc/nginx/sites-available/staging.yourdomain.com
server {
    listen 80;
    server_name staging.yourdomain.com;

    location / {
        proxy_pass http://localhost:3002;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# /etc/nginx/sites-available/yourdomain.com
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Symbolic Links Setup

# Create symbolic links for sites-enabled
sudo ln -s /etc/nginx/sites-available/dev.yourdomain.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/staging.yourdomain.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/

# Test NGINX configuration
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

SSL Certificate Setup

# Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx

# Obtain SSL certificates for all domains
sudo certbot --nginx -d dev.yourdomain.com
sudo certbot --nginx -d staging.yourdomain.com
sudo certbot --nginx -d yourdomain.com

# Set up auto-renewal
sudo crontab -e
# Add this line: 0 12 * * * /usr/bin/certbot renew --quiet

PM2 Process Management

PM2 Configuration

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "app-dev",
      script: "dist/index.js",
      instances: 1,
      exec_mode: "fork",
      env: {
        NODE_ENV: "development",
        PORT: 3001,
      },
      max_memory_restart: "512M",
      error_file: "./logs/err-dev.log",
      out_file: "./logs/out-dev.log",
      log_file: "./logs/combined-dev.log",
      time: true,
    },
    {
      name: "app-staging",
      script: "dist/index.js",
      instances: 2,
      exec_mode: "cluster",
      env: {
        NODE_ENV: "staging",
        PORT: 3002,
      },
      max_memory_restart: "1G",
      error_file: "./logs/err-staging.log",
      out_file: "./logs/out-staging.log",
      log_file: "./logs/combined-staging.log",
      time: true,
    },
    {
      name: "app",
      script: "dist/index.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      max_memory_restart: "2G",
      error_file: "./logs/err.log",
      out_file: "./logs/out.log",
      log_file: "./logs/combined.log",
      time: true,
      autorestart: true,
      watch: false,
      ignore_watch: ["node_modules", "logs"],
    },
  ],
};

PM2 Commands

# Start applications
pm2 start ecosystem.config.js --env development
pm2 start ecosystem.config.js --env staging
pm2 start ecosystem.config.js --env production

# Monitor processes
pm2 list
pm2 monit
pm2 logs

# Restart applications
pm2 restart app-dev
pm2 restart app-staging
pm2 restart app

# Stop applications
pm2 stop app-dev
pm2 stop app-staging
pm2 stop app

# Delete applications
pm2 delete app-dev
pm2 delete app-staging
pm2 delete app

Deployment Automation Scripts

Automated Deployment Script

#!/bin/bash
# deploy.sh

# Configuration
APP_DIR="/var/www/app"
BRANCH="main"
PM2_APP_NAME="app"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${YELLOW}Starting deployment process...${NC}"

# Navigate to application directory
echo -e "${YELLOW}Changing to application directory...${NC}"
cd $APP_DIR

if [ $? -ne 0 ]; then
    echo -e "${RED}Failed to change directory to $APP_DIR${NC}"
    exit 1
fi

# Pull latest changes
echo -e "${YELLOW}Pulling latest changes from $BRANCH...${NC}"
git pull origin $BRANCH

if [ $? -ne 0 ]; then
    echo -e "${RED}Failed to pull latest changes${NC}"
    exit 1
fi

# Install dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
npm install

if [ $? -ne 0 ]; then
    echo -e "${RED}Failed to install dependencies${NC}"
    exit 1
fi

# Build application
echo -e "${YELLOW}Building application...${NC}"
npm run build

if [ $? -ne 0 ]; then
    echo -e "${RED}Failed to build application${NC}"
    exit 1
fi

# Restart PM2 process
echo -e "${YELLOW}Restarting PM2 process...${NC}"
pm2 restart $PM2_APP_NAME

if [ $? -ne 0 ]; then
    echo -e "${RED}Failed to restart PM2 process${NC}"
    exit 1
fi

echo -e "${GREEN}Deployment completed successfully!${NC}"
echo -e "${GREEN}Application is now running on the latest version.${NC}"

Environment-Specific Deployment Scripts

#!/bin/bash
# deploy-dev.sh

APP_DIR="/var/www/app-dev"
BRANCH="develop"
PM2_APP_NAME="app-dev"

# Same deployment logic as above
cd $APP_DIR
git pull origin $BRANCH
npm install
npm run build
pm2 restart $PM2_APP_NAME
#!/bin/bash
# deploy-staging.sh

APP_DIR="/var/www/app-staging"
BRANCH="staging"
PM2_APP_NAME="app-staging"

# Same deployment logic as above
cd $APP_DIR
git pull origin $BRANCH
npm install
npm run build
pm2 restart $PM2_APP_NAME

NGINX Advanced Configuration

# /etc/nginx/sites-available/yourdomain.com
upstream app_backend {
    least_conn;
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3002 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/m;

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Security Headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';";

    # Gzip Compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;

    # Main Application
    location / {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 86400;
    }

    # API Rate Limiting
    location /api/ {
        limit_req zone=api burst=10 nodelay;
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Login Rate Limiting
    location /api/auth/login {
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Static Files
    location /static/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        proxy_pass http://app_backend;
    }

    # Health Check
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

PM2 Advanced Configuration

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "web-app",
      script: "dist/index.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "development",
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      // Advanced PM2 Configuration
      max_memory_restart: "1G",
      min_uptime: "10s",
      max_restarts: 10,
      restart_delay: 4000,
      autorestart: true,
      watch: false,
      ignore_watch: ["node_modules", "logs"],
      error_file: "./logs/err.log",
      out_file: "./logs/out.log",
      log_file: "./logs/combined.log",
      time: true,
      merge_logs: true,
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",

      // Health Check
      health_check_grace_period: 3000,
      health_check_fatal_exceptions: true,

      // Metrics
      pmx: true,

      // Environment Variables
      env_file: ".env.production",

      // Cron Jobs
      cron_restart: "0 2 * * *",

      // Kill Timeout
      kill_timeout: 5000,

      // Listen Timeout
      listen_timeout: 3000,

      // Source Map Support
      source_map_support: true,

      // Node Arguments
      node_args: "--max-old-space-size=4096",
    },
  ],

  deploy: {
    production: {
      user: "ubuntu",
      host: "your-server.com",
      ref: "origin/main",
      repo: "git@github.com:yourusername/your-repo.git",
      path: "/var/www/production",
      "pre-deploy-local": "",
      "post-deploy":
        "npm install && pm2 reload ecosystem.config.js --env production",
      "pre-setup": "",
    },
    staging: {
      user: "ubuntu",
      host: "staging.your-server.com",
      ref: "origin/develop",
      repo: "git@github.com:yourusername/your-repo.git",
      path: "/var/www/staging",
      "pre-deploy-local": "",
      "post-deploy":
        "npm install && pm2 reload ecosystem.config.js --env staging",
      "pre-setup": "",
    },
  },
};

Results & Impact

  • Reduced deployment time from 30 minutes to under 4 minutes with CI/CD.
  • HTTPS enabled via Certbot with auto-renewal.
  • Reusable Bash scripts for any new service or project.
  • Smooth developer onboarding: docker-compose up and you're running.
  • Reliable, production-grade hosting with zero downtime restarts.

What I Learned

  • Deepened understanding of CI/CD and process orchestration with PM2.
  • Improved automation mindset: from config to deployment.
  • Bridged Linux server management with scalable DevOps practice.

Technical Stack

DevOps Tools

  • Docker - Multi-environment containerization
  • GitHub Actions - Automated CI/CD pipelines
  • PM2 - Process management and clustering
  • NGINX - Reverse proxy and domain routing

Infrastructure

  • DigitalOcean VPS - Cloud hosting and server management
  • Linux (Ubuntu) - Server operating system
  • Bash Scripts - Deployment automation
  • SSH - Secure remote server access

Domain & Server Management

  • NGINX sites-available/sites-enabled - Domain configuration
  • Symbolic Links - Domain routing and file management
  • Certbot - SSL certificate automation
  • HTTPS - Secure communication

Development & Deployment

  • Node.js - Application runtime
  • npm - Package management
  • Git - Version control and branching
  • Multi-environment deployment - Dev, staging, production

Links

Note: The codebase for this project is confidential and not publicly available.