Skip to content

Deployment

Deploy applications safely and reliably.

Deployment Strategies

Rolling Deployment

Update instances gradually, one at a time.

Time →
┌───┐ ┌───┐ ┌───┐     ┌───┐ ┌───┐ ┌───┐
│v1 │ │v1 │ │v1 │  →  │v2 │ │v1 │ │v1 │
└───┘ └───┘ └───┘     └───┘ └───┘ └───┘
                   →  │v2 │ │v2 │ │v1 │
                   →  │v2 │ │v2 │ │v2 │

Pros: Zero downtime, gradual rollout Cons: Mixed versions during deployment

Blue-Green Deployment

Switch traffic between two identical environments.

        ┌─────────────┐
        │ Load        │
        │ Balancer    │
        └──────┬──────┘
    ┌──────────┴──────────┐
    │                     │
┌───┴───┐             ┌───┴───┐
│ Blue  │             │ Green │
│  v1   │  ← active   │  v2   │  ← standby
└───────┘             └───────┘

Pros: Instant rollback, full testing before switch Cons: Double infrastructure cost

Canary Deployment

Route small percentage of traffic to new version.

                    100% traffic
                   ┌─────┴─────┐
                   │   Router  │
                   └─────┬─────┘
           ┌─────────────┼─────────────┐
           │             │             │
        95% ↓         5% ↓          0% ↓
      ┌─────┐       ┌─────┐       ┌─────┐
      │ v1  │       │ v2  │       │ v2  │
      └─────┘       └─────┘       └─────┘
      stable        canary        scaling

Pros: Minimize blast radius, real traffic testing Cons: Complex routing, monitoring required

Kubernetes Deployment

Rolling Update

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Extra pod during update
      maxUnavailable: 0  # No downtime
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: api:v2
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 5

GitHub Actions Deployment

deploy:
  runs-on: ubuntu-latest
  needs: build
  environment:
    name: production
    url: https://api.example.com

  steps:
    - uses: actions/checkout@v4

    - name: Configure kubectl
      uses: azure/k8s-set-context@v3
      with:
        kubeconfig: ${{ secrets.KUBECONFIG }}

    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/api \
          api=ghcr.io/${{ github.repository }}:${{ github.sha }}

        kubectl rollout status deployment/api --timeout=5m

Rollback

rollback:
  runs-on: ubuntu-latest
  environment: production

  steps:
    - name: Rollback deployment
      run: kubectl rollout undo deployment/api

Docker Compose Deployment

Deploy Script

#!/bin/bash
# deploy.sh

set -e

COMPOSE_FILE="docker-compose.prod.yml"
IMAGE_TAG="${1:-latest}"

echo "Deploying version: $IMAGE_TAG"

# Pull new images
docker compose -f $COMPOSE_FILE pull

# Deploy with zero downtime
docker compose -f $COMPOSE_FILE up -d --no-deps --scale api=2 api

# Wait for health check
sleep 10

# Remove old containers
docker compose -f $COMPOSE_FILE up -d --no-deps --scale api=1 api

echo "Deployment complete"

GitHub Actions

deploy:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - uses: actions/checkout@v4

    - name: Deploy to server
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.DEPLOY_HOST }}
        username: ${{ secrets.DEPLOY_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /opt/app
          git pull
          docker compose pull
          docker compose up -d

Cloud Platform Deployments

AWS ECS

deploy-ecs:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    - name: Deploy to ECS
      run: |
        aws ecs update-service \
          --cluster production \
          --service api \
          --force-new-deployment

Google Cloud Run

deploy-cloudrun:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - uses: google-github-actions/auth@v2
      with:
        credentials_json: ${{ secrets.GCP_CREDENTIALS }}

    - uses: google-github-actions/deploy-cloudrun@v2
      with:
        service: api
        image: gcr.io/${{ secrets.GCP_PROJECT }}/api:${{ github.sha }}
        region: us-central1

Vercel

deploy-vercel:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - uses: actions/checkout@v4

    - name: Deploy to Vercel
      uses: amondnet/vercel-action@v25
      with:
        vercel-token: ${{ secrets.VERCEL_TOKEN }}
        vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
        vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
        vercel-args: '--prod'

Database Migrations

Run Migrations Before Deploy

deploy:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - uses: actions/checkout@v4

    - name: Run migrations
      run: |
        alembic upgrade head
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}

    - name: Deploy application
      run: ./deploy.sh

Migration Safety Checks

- name: Check migration safety
  run: |
    # Check for destructive migrations
    if alembic history -r head:-1 | grep -i "drop\|delete"; then
      echo "WARNING: Destructive migration detected!"
      exit 1
    fi

Health Checks

Readiness vs Liveness

# health.py
from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/health/live")
async def liveness():
    """Is the process running?"""
    return {"status": "alive"}

@app.get("/health/ready")
async def readiness():
    """Is the app ready to serve traffic?"""
    # Check dependencies
    try:
        await check_database()
        await check_redis()
        return {"status": "ready"}
    except Exception as e:
        return Response(
            content={"status": "not ready", "error": str(e)},
            status_code=503
        )

Wait for Healthy

- name: Deploy
  run: kubectl apply -f deployment.yaml

- name: Wait for rollout
  run: |
    kubectl rollout status deployment/api --timeout=5m

    # Additional health check
    for i in {1..30}; do
      if curl -s https://api.example.com/health/ready | grep -q "ready"; then
        echo "Deployment healthy"
        exit 0
      fi
      sleep 10
    done
    echo "Deployment unhealthy"
    exit 1

Rollback Procedures

Automatic Rollback on Failure

deploy:
  runs-on: ubuntu-latest
  steps:
    - name: Deploy
      id: deploy
      run: |
        kubectl set image deployment/api api=myapp:${{ github.sha }}
        kubectl rollout status deployment/api --timeout=5m

    - name: Rollback on failure
      if: failure() && steps.deploy.outcome == 'failure'
      run: |
        kubectl rollout undo deployment/api
        echo "Deployment failed, rolled back to previous version"

Manual Rollback Workflow

name: Rollback

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to rollback'
        required: true
        type: choice
        options:
          - staging
          - production
      version:
        description: 'Version to rollback to (leave empty for previous)'
        required: false

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}

    steps:
      - name: Rollback
        run: |
          if [ -n "${{ inputs.version }}" ]; then
            kubectl set image deployment/api api=myapp:${{ inputs.version }}
          else
            kubectl rollout undo deployment/api
          fi

Deployment Notifications

Slack Notification

- name: Notify Slack
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "Deployment to ${{ inputs.environment }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Deployment Complete* :rocket:\n*Environment:* ${{ inputs.environment }}\n*Version:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

GitHub Deployment Status

- name: Create deployment
  uses: chrnorm/deployment-action@v2
  id: deployment
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    environment: production

- name: Deploy
  run: ./deploy.sh

- name: Update deployment status
  uses: chrnorm/deployment-status@v2
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    deployment-id: ${{ steps.deployment.outputs.deployment_id }}
    state: ${{ job.status }}
    environment-url: https://api.example.com

Complete Deployment Workflow

name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options: [staging, production]
        default: staging

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to staging
        run: ./deploy.sh staging

      - name: Run smoke tests
        run: ./smoke-tests.sh https://staging.example.com

  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: ./deploy.sh production

      - name: Verify deployment
        run: ./smoke-tests.sh https://example.com

      - name: Rollback on failure
        if: failure()
        run: ./rollback.sh production

Best Practices Summary

Practice Benefit
Use environments Control access, require approval
Health checks Ensure deployment success
Rollback automation Quick recovery
Canary releases Reduce blast radius
Run migrations first Database consistency
Notify on deploy Team awareness
Smoke tests Verify functionality
Immutable deploys Reproducibility