Mastering deployment automation is crucial for modern applications. This guide covers comprehensive PM2 deployment strategies, CI/CD integration, and advanced automation techniques for production environments.
Zero-Downtime Deployment with PM2
PM2’s reload feature enables zero-downtime deployments by gracefully restarting application instances one by one, ensuring continuous service availability.
// Graceful shutdown implementation
const express = require('express');
const app = express();
let isShuttingDown = false;
// Health check endpoint
app.get('/health', (req, res) => {
if (isShuttingDown) {
return res.status(503).json({
status: 'shutting_down'
});
}
res.json({
status: 'healthy',
uptime: process.uptime(),
pid: process.pid
});
});
// Graceful shutdown handler
const gracefulShutdown = (signal) => {
console.log(`Received ${signal}, starting graceful shutdown...`);
isShuttingDown = true;
server.close((err) => {
if (err) {
console.error('Error during server shutdown:', err);
process.exit(1);
}
console.log('HTTP server closed');
process.exit(0);
});
// Force shutdown after 30 seconds
setTimeout(() => {
console.error('Could not close connections in time, forcefully shutting down');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
const server = app.listen(process.env.PORT || 3000);
Advanced Deployment Script
#!/bin/bash
# advanced-deploy.sh
APP_NAME="myapp"
HEALTH_URL="http://localhost:8000/health"
LOG_FILE="/var/log/myapp/deployment.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
check_prerequisites() {
log "Checking deployment prerequisites..."
if ! pm2 list > /dev/null 2>&1; then
log "ERROR: PM2 is not running"
return 1
fi
if ! pm2 describe $APP_NAME > /dev/null 2>&1; then
log "ERROR: Application '$APP_NAME' not found in PM2"
return 1
fi
log "Prerequisites check completed"
return 0
}
update_application() {
log "Updating application code..."
if ! git pull origin main; then
log "ERROR: Failed to pull latest changes"
return 1
fi
if ! npm ci --production; then
log "ERROR: Failed to install dependencies"
return 1
fi
log "Application update completed"
return 0
}
perform_pm2_reload() {
log "Performing PM2 reload..."
if pm2 reload $APP_NAME --update-env; then
log "PM2 reload completed successfully"
return 0
else
log "ERROR: PM2 reload failed"
return 1
fi
}
check_health() {
local retries=10
local count=0
log "Performing health checks..."
while [ $count -lt $retries ]; do
count=$((count + 1))
if curl -f -s $HEALTH_URL > /dev/null 2>&1; then
log "Health check passed"
return 0
fi
log "Health check $count/$retries failed, retrying..."
sleep 5
done
log "ERROR: Health checks failed"
return 1
}
main() {
log "Starting deployment process"
if ! check_prerequisites; then
exit 1
fi
if ! update_application; then
exit 1
fi
if ! perform_pm2_reload; then
exit 1
fi
if ! check_health; then
exit 1
fi
log "Deployment completed successfully!"
}
main "$@"
GitHub Actions CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy Application
on:
push:
branches: [ main ]
env:
NODE_VERSION: '18'
PM2_APP_NAME: 'myapp'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.8
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USERNAME }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/production
git pull origin main
npm ci --production
pm2 reload myapp --update-env
sleep 30
# Health check
if curl -f http://localhost:8000/health; then
echo "Deployment successful"
pm2 save
else
echo "Deployment failed"
exit 1
fi
graph TD A[Code Commit] --> B[CI/CD Trigger] B --> C[Run Tests] C --> D{Tests Pass?} D -->|No| E[Deployment Failed] D -->|Yes| F[Deploy to Server] F --> G[PM2 Reload] G --> H[Health Checks] H --> I{Health OK?} I -->|No| J[Rollback] I -->|Yes| K[Deployment Success]
Blue-Green Deployment
Blue-green deployment provides instant rollback capability by maintaining two identical environments.
#!/bin/bash
# blue-green-deploy.sh
BLUE_PORT=8000
GREEN_PORT=8001
NGINX_CONFIG="/etc/nginx/conf.d/myapp.conf"
get_current_environment() {
if grep -q ":$BLUE_PORT" $NGINX_CONFIG; then
echo "blue"
else
echo "green"
fi
}
deploy_to_environment() {
local env=$1
local port=$2
local app_dir="/var/www/myapp-$env"
echo "Deploying to $env environment"
cd $app_dir
git pull origin main
npm ci --production
pm2 delete "myapp-$env" 2>/dev/null || true
PORT=$port pm2 start ecosystem.config.js --name "myapp-$env"
# Health check
sleep 20
if curl -f "http://localhost:$port/health"; then
echo "$env environment is healthy"
return 0
else
echo "$env environment health check failed"
return 1
fi
}
switch_traffic() {
local target_env=$1
local port=$2
echo "Switching traffic to $target_env environment"
# Update nginx configuration
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*;/proxy_pass http:\/\/localhost:$port;/" $NGINX_CONFIG
# Reload nginx
nginx -t && systemctl reload nginx
}
main() {
current_env=$(get_current_environment)
if [ "$current_env" = "blue" ]; then
target_env="green"
target_port=$GREEN_PORT
else
target_env="blue"
target_port=$BLUE_PORT
fi
echo "Current: $current_env, Target: $target_env"
if deploy_to_environment $target_env $target_port; then
switch_traffic $target_env $target_port
echo "Deployment successful"
else
echo "Deployment failed"
exit 1
fi
}
main
Monitoring During Deployment
#!/bin/bash
# deployment-monitor.sh
HEALTH_URL="http://localhost:8000/health"
MONITORING_DURATION=300 # 5 minutes
monitor_deployment() {
local start_time=$(date +%s)
local end_time=$((start_time + MONITORING_DURATION))
local failure_count=0
echo "Starting deployment monitoring..."
while [ $(date +%s) -lt $end_time ]; do
if curl -f -s $HEALTH_URL > /dev/null; then
echo "Health check passed"
failure_count=0
else
failure_count=$((failure_count + 1))
echo "Health check failed ($failure_count)"
if [ $failure_count -ge 3 ]; then
echo "Too many failures, deployment monitoring failed"
return 1
fi
fi
sleep 10
done
echo "Deployment monitoring completed successfully"
return 0
}
monitor_deployment
Production Best Practices
- Zero-Downtime Deployments: Always use PM2 reload for seamless updates
- Health Checks: Implement comprehensive health check endpoints
- Rollback Strategy: Maintain previous versions for quick rollback
- Monitoring: Set up alerts for deployment failures
- Testing: Run automated tests before deployment
- Gradual Rollout: Use blue-green or canary deployments for large changes
Series Complete! You now have comprehensive PM2 knowledge from basics to advanced deployment automation. This series covered fundamentals, configuration, clustering, systemd integration, monitoring, and deployment automation.