Deployment Patterns
Production deployment workflows and CI/CD best practices.
When to Activate
- Setting up CI/CD pipelines
- Dockerizing an application
- Planning deployment strategy (blue-green, canary, rolling)
- Implementing health checks and readiness probes
- Preparing for a production release
- Configuring environment-specific settings
Deployment Strategies
Rolling Deployment (Default)
Replace instances gradually — old and new versions run simultaneously during rollout.
Instance 1: v1 → v2 (update first)
Instance 2: v1 (still running v1)
Instance 3: v1 (still running v1)
Instance 1: v2
Instance 2: v1 → v2 (update second)
Instance 3: v1
Instance 1: v2
Instance 2: v2
Instance 3: v1 → v2 (update last)
Pros: Zero downtime, gradual rollout Cons: Two versions run simultaneously — requires backward-compatible changes Use when: Standard deployments, backward-compatible changes
Blue-Green Deployment
Run two identical environments. Switch traffic atomically.
Blue (v1) ← traffic
Green (v2) idle, running new version
# After verification:
Blue (v1) idle (becomes standby)
Green (v2) ← traffic
Pros: Instant rollback (switch back to blue), clean cutover Cons: Requires 2x infrastructure during deployment Use when: Critical services, zero-tolerance for issues
Canary Deployment
Route a small percentage of traffic to the new version first.
v1: 95% of traffic
v2: 5% of traffic (canary)
# If metrics look good:
v1: 50% of traffic
v2: 50% of traffic
# Final:
v2: 100% of traffic
Pros: Catches issues with real traffic before full rollout Cons: Requires traffic splitting infrastructure, monitoring Use when: High-traffic services, risky changes, feature flags
Docker
Multi-Stage Dockerfile (Node.js)
dockerfile1# Stage 1: Install dependencies 2FROM node:22-alpine AS deps 3WORKDIR /app 4COPY package.json package-lock.json ./ 5RUN npm ci --production=false 6 7# Stage 2: Build 8FROM node:22-alpine AS builder 9WORKDIR /app 10COPY --from=deps /app/node_modules ./node_modules 11COPY . . 12RUN npm run build 13RUN npm prune --production 14 15# Stage 3: Production image 16FROM node:22-alpine AS runner 17WORKDIR /app 18 19RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 20USER appuser 21 22COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules 23COPY --from=builder --chown=appuser:appgroup /app/dist ./dist 24COPY --from=builder --chown=appuser:appgroup /app/package.json ./ 25 26ENV NODE_ENV=production 27EXPOSE 3000 28 29HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 30 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 31 32CMD ["node", "dist/server.js"]
Multi-Stage Dockerfile (Go)
dockerfile1FROM golang:1.22-alpine AS builder 2WORKDIR /app 3COPY go.mod go.sum ./ 4RUN go mod download 5COPY . . 6RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server 7 8FROM alpine:3.19 AS runner 9RUN apk --no-cache add ca-certificates 10RUN adduser -D -u 1001 appuser 11USER appuser 12 13COPY --from=builder /server /server 14 15EXPOSE 8080 16HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:8080/health || exit 1 17CMD ["/server"]
Multi-Stage Dockerfile (Python/Django)
dockerfile1FROM python:3.12-slim AS builder 2WORKDIR /app 3RUN pip install --no-cache-dir uv 4COPY requirements.txt . 5RUN uv pip install --system --no-cache -r requirements.txt 6 7FROM python:3.12-slim AS runner 8WORKDIR /app 9 10RUN useradd -r -u 1001 appuser 11USER appuser 12 13COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages 14COPY --from=builder /usr/local/bin /usr/local/bin 15COPY . . 16 17ENV PYTHONUNBUFFERED=1 18EXPOSE 8000 19 20HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" || exit 1 21CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
Docker Best Practices
# GOOD practices
- Use specific version tags (node:22-alpine, not node:latest)
- Multi-stage builds to minimize image size
- Run as non-root user
- Copy dependency files first (layer caching)
- Use .dockerignore to exclude node_modules, .git, tests
- Add HEALTHCHECK instruction
- Set resource limits in docker-compose or k8s
# BAD practices
- Running as root
- Using :latest tags
- Copying entire repo in one COPY layer
- Installing dev dependencies in production image
- Storing secrets in image (use env vars or secrets manager)
CI/CD Pipeline
GitHub Actions (Standard Pipeline)
yaml1name: CI/CD 2 3on: 4 push: 5 branches: [main] 6 pull_request: 7 branches: [main] 8 9jobs: 10 test: 11 runs-on: ubuntu-latest 12 steps: 13 - uses: actions/checkout@v4 14 - uses: actions/setup-node@v4 15 with: 16 node-version: 22 17 cache: npm 18 - run: npm ci 19 - run: npm run lint 20 - run: npm run typecheck 21 - run: npm test -- --coverage 22 - uses: actions/upload-artifact@v4 23 if: always() 24 with: 25 name: coverage 26 path: coverage/ 27 28 build: 29 needs: test 30 runs-on: ubuntu-latest 31 if: github.ref == 'refs/heads/main' 32 steps: 33 - uses: actions/checkout@v4 34 - uses: docker/setup-buildx-action@v3 35 - uses: docker/login-action@v3 36 with: 37 registry: ghcr.io 38 username: ${{ github.actor }} 39 password: ${{ secrets.GITHUB_TOKEN }} 40 - uses: docker/build-push-action@v5 41 with: 42 push: true 43 tags: ghcr.io/${{ github.repository }}:${{ github.sha }} 44 cache-from: type=gha 45 cache-to: type=gha,mode=max 46 47 deploy: 48 needs: build 49 runs-on: ubuntu-latest 50 if: github.ref == 'refs/heads/main' 51 environment: production 52 steps: 53 - name: Deploy to production 54 run: | 55 # Platform-specific deployment command 56 # Railway: railway up 57 # Vercel: vercel --prod 58 # K8s: kubectl set image deployment/app app=ghcr.io/${{ github.repository }}:${{ github.sha }} 59 echo "Deploying ${{ github.sha }}"
Pipeline Stages
PR opened:
lint → typecheck → unit tests → integration tests → preview deploy
Merged to main:
lint → typecheck → unit tests → integration tests → build image → deploy staging → smoke tests → deploy production
Health Checks
Health Check Endpoint
typescript1// Simple health check 2app.get("/health", (req, res) => { 3 res.status(200).json({ status: "ok" }); 4}); 5 6// Detailed health check (for internal monitoring) 7app.get("/health/detailed", async (req, res) => { 8 const checks = { 9 database: await checkDatabase(), 10 redis: await checkRedis(), 11 externalApi: await checkExternalApi(), 12 }; 13 14 const allHealthy = Object.values(checks).every(c => c.status === "ok"); 15 16 res.status(allHealthy ? 200 : 503).json({ 17 status: allHealthy ? "ok" : "degraded", 18 timestamp: new Date().toISOString(), 19 version: process.env.APP_VERSION || "unknown", 20 uptime: process.uptime(), 21 checks, 22 }); 23}); 24 25async function checkDatabase(): Promise<HealthCheck> { 26 try { 27 await db.query("SELECT 1"); 28 return { status: "ok", latency_ms: 2 }; 29 } catch (err) { 30 return { status: "error", message: "Database unreachable" }; 31 } 32}
Kubernetes Probes
yaml1livenessProbe: 2 httpGet: 3 path: /health 4 port: 3000 5 initialDelaySeconds: 10 6 periodSeconds: 30 7 failureThreshold: 3 8 9readinessProbe: 10 httpGet: 11 path: /health 12 port: 3000 13 initialDelaySeconds: 5 14 periodSeconds: 10 15 failureThreshold: 2 16 17startupProbe: 18 httpGet: 19 path: /health 20 port: 3000 21 initialDelaySeconds: 0 22 periodSeconds: 5 23 failureThreshold: 30 # 30 * 5s = 150s max startup time
Environment Configuration
Twelve-Factor App Pattern
bash1# All config via environment variables — never in code 2DATABASE_URL=postgres://user:pass@host:5432/db 3REDIS_URL=redis://host:6379/0 4API_KEY=${API_KEY} # injected by secrets manager 5LOG_LEVEL=info 6PORT=3000 7 8# Environment-specific behavior 9NODE_ENV=production # or staging, development 10APP_ENV=production # explicit app environment
Configuration Validation
typescript1import { z } from "zod"; 2 3const envSchema = z.object({ 4 NODE_ENV: z.enum(["development", "staging", "production"]), 5 PORT: z.coerce.number().default(3000), 6 DATABASE_URL: z.string().url(), 7 REDIS_URL: z.string().url(), 8 JWT_SECRET: z.string().min(32), 9 LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), 10}); 11 12// Validate at startup — fail fast if config is wrong 13export const env = envSchema.parse(process.env);
Rollback Strategy
Instant Rollback
bash1# Docker/Kubernetes: point to previous image 2kubectl rollout undo deployment/app 3 4# Vercel: promote previous deployment 5vercel rollback 6 7# Railway: redeploy previous commit 8railway up --commit <previous-sha> 9 10# Database: rollback migration (if reversible) 11npx prisma migrate resolve --rolled-back <migration-name>
Rollback Checklist
- Previous image/artifact is available and tagged
- Database migrations are backward-compatible (no destructive changes)
- Feature flags can disable new features without deploy
- Monitoring alerts configured for error rate spikes
- Rollback tested in staging before production release
Production Readiness Checklist
Before any production deployment:
Application
- All tests pass (unit, integration, E2E)
- No hardcoded secrets in code or config files
- Error handling covers all edge cases
- Logging is structured (JSON) and does not contain PII
- Health check endpoint returns meaningful status
Infrastructure
- Docker image builds reproducibly (pinned versions)
- Environment variables documented and validated at startup
- Resource limits set (CPU, memory)
- Horizontal scaling configured (min/max instances)
- SSL/TLS enabled on all endpoints
Monitoring
- Application metrics exported (request rate, latency, errors)
- Alerts configured for error rate > threshold
- Log aggregation set up (structured logs, searchable)
- Uptime monitoring on health endpoint
Security
- Dependencies scanned for CVEs
- CORS configured for allowed origins only
- Rate limiting enabled on public endpoints
- Authentication and authorization verified
- Security headers set (CSP, HSTS, X-Frame-Options)
Operations
- Rollback plan documented and tested
- Database migration tested against production-sized data
- Runbook for common failure scenarios
- On-call rotation and escalation path defined