Deployment & CI/CD
Deployment & CI/CD
Section titled “Deployment & CI/CD”📖 Introduction
Section titled “📖 Introduction”Deployment is the process of taking your application from a development environment to production where users can access it. A robust deployment pipeline includes: automated testing, building, containerization, configuration injection, and zero-downtime rollouts.
CI/CD (Continuous Integration / Continuous Deployment) automates this process — every commit to the main branch triggers automated tests, and if they pass, the code is automatically deployed to production.
🤔 Why Do We Need This?
Section titled “🤔 Why Do We Need This?”Manual deployment is error-prone, slow, and inconsistent:
# ❌ Manual deployment (slow, error-prone)ssh prod-servergit pullnpm install# Hope nothing breaks!pm2 restart all
# ✅ CI/CD (automated, reliable)git push origin main# → GitHub Actions runs tests# → Builds Docker image# → Deploys to production# → Health check verifies it's working⚠️ Problem Statement
Section titled “⚠️ Problem Statement”A production deployment system must handle:
- Zero-downtime — Users shouldn’t see errors during deployment
- Rollback — If something goes wrong, revert to the previous version instantly
- Environment injection — Different config for dev/staging/prod without code changes
- Health verification — Ensure the new version is healthy before routing traffic
- Secrets management — API keys and passwords must be injected securely
- Database migrations — Schema changes must be coordinated with deployments
📚 Real World Story
Section titled “📚 Real World Story”Etsy deploys 50+ times per day. Their continuous deployment pipeline was a key factor in their engineering culture transformation. Each developer pushes to main multiple times daily, and within minutes, their code is running in production.
Key to their success: feature flags. Not every deployment immediately enables the new feature. Code is deployed continuously, but features are turned on gradually (10%, 50%, 100% of users) and can be instantly disabled if problems arise. This decouples deployment from release.
🍕 Real World Analogy
Section titled “🍕 Real World Analogy”| Deployment Concept | Airline Analogy |
|---|---|
| CI/CD pipeline | Pre-flight checklist |
| Build stage | Assembling the plane |
| Test stage | Test flight |
| Staging | Training simulator |
| Production | Revenue flight |
| Blue-green deploy | Two parallel runways — switch between them |
| Canary release | Test a new engine on one plane first |
| Rollback | Return to the gate if something’s wrong |
| Health check | Instrument panel showing all systems OK |
👁️ Visual Explanation
Section titled “👁️ Visual Explanation”Deployment Pipeline Flow:
[Code Push] → [Tests] → [Build] → [Stage] → [Deploy] → [Verify] │ │ │ │ │ │ │ ▼ │ │ │ │ │ Tests fail │ │ │ │ │ ❌ Notify dev │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ Build image │ │ │ │ Push to reg. │ │ │ │ ▼ │ │ │ Deploy to │ │ │ staging │ │ │ Run E2E tests │ │ │ ▼ │ │ Deploy to │ │ production │ │ ▼ │ Health check │ ✅ All good!📊 Mermaid Diagram 1: CI/CD Pipeline
Section titled “📊 Mermaid Diagram 1: CI/CD Pipeline”flowchart LR A["👨💻 Developer Pushes Code"] --> B["📦 GitHub Actions Triggers"] B --> C["🔍 Lint & Type Check"] C --> D["🧪 Run Tests"] D --> E{"Tests Pass?"} E -->|"No"| F["❌ Notify Developer"] E -->|"Yes"| G["🏗️ Build Docker Image"] G --> H["📤 Push to Registry"] H --> I["🧪 Deploy to Staging"] I --> J["🔬 Run E2E Tests"] J --> K{"E2E Pass?"} K -->|"No"| F K -->|"Yes"| L["🚀 Deploy to Production"] L --> M["💚 Health Check"] M --> N["✅ Done"]
style A fill:#4f46e5,color:#fff style L fill:#059669,color:#fff style F fill:#dc2626,color:#fff style N fill:#10b981,color:#fff⚙️ Internal Working: How a CI/CD Pipeline Processes a Commit
Section titled “⚙️ Internal Working: How a CI/CD Pipeline Processes a Commit”- Trigger: A push to the
mainbranch triggers a webhook to GitHub Actions - Checkout: The CI runner clones the repository
- Setup: Node.js is installed, dependencies are cached and installed (
npm ci) - Lint: ESLint checks code quality, TypeScript checks types
- Test: Jest runs unit + integration tests with coverage
- Build: Docker image is built with the application code
- Push: Image is pushed to a container registry (Docker Hub, ECR, GCR)
- Deploy staging: Container is deployed to the staging environment
- E2E tests: Cypress or Playwright tests run against the staging environment
- Deploy production: If all checks pass, deployment to production begins
🔄 Mermaid Diagram 2: Blue-Green Deployment
Section titled “🔄 Mermaid Diagram 2: Blue-Green Deployment”sequenceDiagram participant LB as Load Balancer participant Blue as Blue (Old Version) participant Green as Green (New Version) participant Monitor as Monitoring
Note over LB,Monitor: Blue is live, serving all traffic
Green->>Green: Deploy new version Green->>Monitor: Health check starts
alt Health check passes Monitor->>LB: Route traffic to Green LB->>Green: All new requests go to Green Blue->>Blue: Drain existing connections Note over Blue,Monitor: Blue becomes idle (rollback target) else Health check fails Monitor->>Blue: Stay on Blue Green->>Green: Auto-rollback, destroy Monitor->>Alert: Notify DevOps end📝 Syntax
Section titled “📝 Syntax”GitHub Actions CI/CD
Section titled “GitHub Actions CI/CD”name: Deployon: push: branches: [main]
jobs: test: runs-on: ubuntu-latest services: mongodb: image: mongo:7 ports: ['27017:27017'] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npm test -- --ci --coverage - run: npm run build
deploy: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to VPS uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSH_KEY }} script: | cd /app git pull npm ci --production pm2 restart all🟢 Basic Example: VPS Deployment with Nginx
Section titled “🟢 Basic Example: VPS Deployment with Nginx”server { listen 80; server_name myapp.com; return 301 https://$host$request_uri;}
server { listen 443 ssl http2; server_name myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Proxy API requests to Node.js location /api { proxy_pass http://localhost:3000; 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;
# Timeouts proxy_connect_timeout 60s; proxy_read_timeout 60s; }
# Serve static files directly location / { root /var/www/myapp/public; try_files $uri $uri/ /index.html; expires 1y; add_header Cache-Control "public, immutable"; }}What’s happening:
- SSL termination — Nginx handles HTTPS, Node.js handles HTTP internally
- Reverse proxy —
/apirequests are forwarded to Node.js on port 3000 - Static files — Served directly by Nginx (more efficient than Node.js)
- WebSocket support —
UpgradeandConnectionheaders passed through - Timeouts — 60s to prevent hanging connections
🟡 Intermediate Example: Docker Multi-Stage Build
Section titled “🟡 Intermediate Example: Docker Multi-Stage Build”# Stage 1: BuildFROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# Stage 2: ProductionFROM node:20-alpine AS productionWORKDIR /app
# Create non-root userRUN addgroup -g 1001 -S appgroup && \ adduser -S appuser -u 1001 -G appgroup
# Copy built filesCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD ["node", "dist/server.js"]What’s happening:
- Multi-stage build — build tools are in stage 1, only runtime in stage 2 (smaller image)
- Non-root user — security best practice (no root inside container)
- HEALTHCHECK — Docker checks every 30s, restarts if 3 consecutive failures
- Alpine base — minimal Linux (~5MB base) for small images
🔴 Advanced Example: Zero-Downtime Deployment Script
Section titled “🔴 Advanced Example: Zero-Downtime Deployment Script”#!/bin/bash# deploy.sh — zero-downtime deployment with PM2
set -e # Exit on error
APP_NAME="myapp"NEW_PORT=3001 # Temporary port for new version
echo "🚀 Starting deployment of $APP_NAME..."
# 1. Pull latest codeecho "📦 Pulling latest code..."git pull origin main
# 2. Install dependenciesecho "📦 Installing dependencies..."npm ci --production
# 3. Buildecho "🏗️ Building..."npm run build
# 4. Start new version on alternate portecho "🔄 Starting new version on port $NEW_PORT..."PORT=$NEW_PORT NODE_ENV=production pm2 start dist/server.js \ --name "${APP_NAME}-new" \ --wait-ready \ --listen-timeout 30000
# 5. Wait for health checkecho "💚 Waiting for health check..."for i in {1..30}; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$NEW_PORT/health) if [ "$STATUS" == "200" ]; then echo "✅ Health check passed!" break fi if [ "$i" == "30" ]; then echo "❌ Health check failed! Rolling back..." pm2 delete "${APP_NAME}-new" exit 1 fi sleep 1done
# 6. Update Nginx to point to new versionecho "🔀 Updating Nginx configuration..."sed -i "s/proxy_pass http:\/\/localhost:3000/proxy_pass http:\/\/localhost:$NEW_PORT/" /etc/nginx/sites-enabled/$APP_NAMEnginx -t && systemctl reload nginx
# 7. Stop old versionecho "🛑 Stopping old version..."pm2 delete "$APP_NAME"pm2 rename "${APP_NAME}-new" "$APP_NAME"
echo "✅ Deployment complete!"What’s happening:
- Staged rollout — new version starts on a different port
- Health check — verifies the new version is working before switching traffic
- Nginx reload — switches traffic without dropping connections
- Rollback on failure — if health check fails, the old version stays running
--wait-ready— PM2 waits for the app to emit a “ready” message
🏭 Production Example: Full CI/CD with GitHub Actions
Section titled “🏭 Production Example: Full CI/CD with GitHub Actions”name: CI/CD Pipeline
on: push: branches: [main, staging] pull_request: branches: [main]
env: NODE_VERSION: '20' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: lint-and-test: runs-on: ubuntu-latest services: mongodb: image: mongo:7 ports: ['27017:27017'] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '${{ env.NODE_VERSION }}' } - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm test -- --ci --coverage env: MONGO_URI: mongodb://localhost:27017/test JWT_SECRET: test-secret - uses: codecov/codecov-action@v3
build-and-push: needs: lint-and-test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v5 with: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
deploy-staging: needs: build-and-push if: github.ref == 'refs/heads/staging' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | echo "Deploying to staging..." # Use your deployment tool (e.g., Helm, kubectl, SSH) # helm upgrade myapp ./helm --set image.tag=${{ github.sha }}
deploy-production: needs: build-and-push if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production steps: - run: | echo "Deploying to production..." # Deploy to production orchestrator⚙️ How It Works Internally: Nginx as a Reverse Proxy
Section titled “⚙️ How It Works Internally: Nginx as a Reverse Proxy”When a request arrives at Nginx:
- TCP connection: Nginx accepts the TCP connection from the client
- TLS termination: If HTTPS, Nginx decrypts the SSL/TLS layer
- Header parsing: Nginx parses HTTP headers (Host, path, etc.)
- Location matching: Nginx matches the URL against
locationblocks - Proxy decision: For
/api, Nginx opens a new connection tolocalhost:3000 - Data forwarding: Nginx streams request data to Node.js
- Response streaming: Nginx streams the response back to the client
This adds ~1-5ms latency but provides: SSL termination, static file serving, load balancing, rate limiting, and security filtering.
📦 Performance Notes
Section titled “📦 Performance Notes”Deployment Strategies Comparison
Section titled “Deployment Strategies Comparison”| Strategy | Downtime | Rollback Speed | Complexity |
|---|---|---|---|
| Rolling update | None | Medium | Medium |
| Blue-green | None | Instant | High |
| Canary | None | Instant | High |
| Replace (stop-start) | 5-30s | Slow | Low |
Startup Time Optimization
Section titled “Startup Time Optimization”// Enable ready signal for PM2/Dockerapp.listen(port, () => { if (process.send) process.send('ready'); // Signal PM2 logger.info(`Server ready on port ${port}`);});🔒 Security Notes
Section titled “🔒 Security Notes”Deployment Security Checklist
Section titled “Deployment Security Checklist”- SSH keys over passwords for server access
- Least privilege — CI/CD tokens should only deploy, not manage infrastructure
- Secrets in CI — use GitHub Actions secrets, not in code
- Signed commits — require GPG signatures for deployments
- Audit trail — every deployment logged with who, what, when
- Environment isolation — production secrets never accessible from staging
⚠️ Common Mistakes
Section titled “⚠️ Common Mistakes”-
❌ Manually deploying — Always use CI/CD. Manual deployments are error-prone and unrepeatable.
-
❌ No health checks — Deployments that break the app go undetected until users complain.
-
❌ Hardcoded environment config — Same Docker image must work in all environments. Config goes in environment variables.
-
❌ No rollback plan — If the deployment fails, you need a way to revert instantly.
-
❌ Deploying during peak hours — Always deploy during low traffic, or use zero-downtime strategies.
-
❌ Database migrations during deployment — Schema changes can break the running version. Plan backward-compatible migrations.
🚀 Best Practices
Section titled “🚀 Best Practices”Deployment Checklist
Section titled “Deployment Checklist”- All tests pass in CI
- Docker image built and pushed to registry
- Health check endpoint configured
- Database migrations run separately (backward-compatible)
- Zero-downtime strategy (blue-green or rolling)
- Rollback plan ready
- Monitoring alerts configured
- Deployment logged and notified to team
Production Dockerfile
Section titled “Production Dockerfile”FROM node:20-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=production && npm cache clean --forceCOPY . .USER nodeEXPOSE 3000HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1CMD ["node", "server.js"]🎯 Interview Questions
Section titled “🎯 Interview Questions”Q1: What’s the difference between blue-green and canary deployments?
Blue-green deploys a full copy of the new version alongside the old, then switches traffic at the load balancer. Canary gradually shifts traffic (e.g., 5% → 25% → 100%) to the new version, monitoring errors and latency at each step. Canary is safer because you can abort early if issues arise, but blue-green is simpler to implement.
Q2: How do you achieve zero-downtime deployments with Node.js?
Start the new version on a different port, verify health, update the reverse proxy (Nginx) to point to the new port, then gracefully shut down the old version. PM2 supports this with --wait-ready and the process.send('ready') signal. The key is having two running instances with a smooth traffic switch.
Q3: How do you handle database migrations during deployment?
Make migrations backward-compatible: add columns before removing old ones, and never break the existing version’s queries. Run migrations as a separate step before deploying new code. Use a tool like umzug or db-migrate that tracks which migrations have been applied.
📝 MCQs
Section titled “📝 MCQs”1. What port does the new version use in a blue-green deployment?
- A) The same port as the old version
- B) A different port to run alongside the old version ✅
- C) Port 80
- D) A random port
2. What does the HEALTHCHECK instruction do in a Dockerfile?
- A) Scans the container for vulnerabilities
- B) Checks if the app is healthy and restarts if not ✅
- C) Logs the container’s health status
- D) Sends health data to a monitoring service
3. What is a canary release?
- A) Deploying to all servers at once
- B) Gradually shifting traffic to a new version ✅
- C) Using a separate staging environment
- D) Rolling back to a previous version
4. Why is multi-stage Docker build useful?
- A) It deploys to multiple servers at once
- B) It separates build tools from runtime, producing smaller images ✅
- C) It allows multiple versions to run simultaneously
- D) It runs tests in parallel
5. Which deployment strategy has the fastest rollback?
- A) Rolling update
- B) Blue-green ✅
- C) Replace
- D) Rolling restart
Answer Key: 1-B, 2-B, 3-B, 4-B, 5-B
💻 Coding Challenge 1: Docker Compose Setup
Section titled “💻 Coding Challenge 1: Docker Compose Setup”Create a docker-compose.yml for a Node.js app with:
- Node.js service (port 3000)
- MongoDB (port 27017)
- Redis (port 6379)
- Nginx reverse proxy (port 80)
- Volume mounts for persistent data
- Environment variables for configuration
💻 Coding Challenge 2: GitHub Actions CI Pipeline
Section titled “💻 Coding Challenge 2: GitHub Actions CI Pipeline”Create a GitHub Actions workflow that:
- Runs on push and PR to main
- Caches node_modules for faster builds
- Runs lint, typecheck, and tests
- Builds and pushes Docker image to GitHub Container Registry
- Has separate deploy jobs for staging and production environments
💻 Coding Challenge 3: Zero-Downtime Deploy Script
Section titled “💻 Coding Challenge 3: Zero-Downtime Deploy Script”Write a bash script that:
- Pulls latest code from Git
- Installs dependencies
- Starts new version on port 3001
- Runs health checks (up to 30 attempts)
- Updates Nginx to proxy to the new port
- Stops the old version
🧪 Mini Exercise: Debugging a Broken Deploy
Section titled “🧪 Mini Exercise: Debugging a Broken Deploy”# You run a deployment script that fails. Identify the issues:
#!/bin/bash# Bug 1: No set -e — What if npm ci fails?npm ci# Bug 2: What if build fails silently?npm run build # Could fail without stopping# Bug 3: Starting on the same port — crashes!PORT=3000 node dist/server.js &# Bug 4: No health check — how do we know it's working?# Bug 5: If start fails, the old version is already stopped!pm2 delete myapp # Old version deleted before new one starts🌍 Real World Problem (Interview Coding Challenge)
Section titled “🌍 Real World Problem (Interview Coding Challenge)”Problem: Design a deployment system for a global e-commerce platform with 50 microservices, deployed across 3 data centers (US, EU, Asia). The platform handles 10K requests/second and must maintain 99.99% uptime.
Requirements:
- Zero-downtime deployments across all data centers
- Rollback in under 30 seconds for any service
- Database migrations must not cause downtime
- Configuration per data center (different currencies, languages)
- Canary deployments with automatic rollback on error rate increase
Questions:
- What deployment architecture would you design?
- How do you coordinate deployments across data centers?
- How do you handle database schema changes without downtime?
- What metrics do you monitor during a canary deployment to decide whether to proceed?
🏗️ Mini Project: Deployment Pipeline
Section titled “🏗️ Mini Project: Deployment Pipeline”Build a complete CI/CD pipeline for a Node.js application:
Core features:
- GitHub Actions workflow with lint, test, build stages
- Docker multi-stage build
- Docker Compose for local development
- Nginx reverse proxy configuration
- PM2 ecosystem config for production
- Zero-downtime deployment script
Bonus features:
- Slack notification on deployment success/failure
- Automatic rollback on health check failure
- Blue-green deployment with Nginx
- Database migration step in CI/CD
📖 Summary
Section titled “📖 Summary”| Concept | Key Takeaway |
|---|---|
| CI/CD | Automate testing and deployment — every commit to main triggers the pipeline |
| Blue-green | Two environments, switch traffic instantly |
| Canary | Gradual traffic shift, monitor at each step |
| Nginx | Reverse proxy, SSL termination, static file serving |
| Docker | Containerize for consistent environments |
| Health check | Verify deployment succeeded before routing traffic |
| Rollback | Always have a plan to revert to the previous version |
📋 Cheat Sheet
Section titled “📋 Cheat Sheet”# Quick reference: Deployment
# Nginx proxy configlocation /api { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade';}
# Docker builddocker build -t myapp:latest .docker run -p 3000:3000 -e DB_URL=mongodb://... myapp
# PM2pm2 start server.js -i maxpm2 reload all # Zero-downtime restart
# Docker Composedocker compose up -ddocker compose logs -f
# CI/CD (GitHub Actions)# .github/workflows/deploy.yml triggers on push to main📚 Further Reading
Section titled “📚 Further Reading”- GitHub Actions Documentation
- Docker Node.js Guide
- PM2 Process Manager
- Nginx Reverse Proxy
- 12-Factor App — Build, Release, Run
🔗 Related Topics
Section titled “🔗 Related Topics”- Configuration & Environment — Environment injection
- Testing — Automated testing in CI/CD
- Containerization — Docker and clustering
- Monitoring — Deployment monitoring