
Sandesh Shrestha
Jan 2025 – Present/Advanced
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 upand 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.