Skip to main content

Deployment Strategies

Once your code passes CI, the next step is getting it to users. In this lesson, you will build deployment workflows that ship your application to staging and production environments safely.

Deployment Environments

GitHub Actions has a built-in concept of environments that provides:

  • Protection rules (required reviewers, wait timers)
  • Environment-specific secrets
  • Deployment history and status

Configure environments in Settings > Environments.

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        run: echo "Deploying to staging..."

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://myapp.com
    steps:
      - name: Deploy to production
        run: echo "Deploying to production..."

A Complete Staging + Production Pipeline

name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false  # Never cancel in-progress deployments

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm test
      - run: npm run build

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: test
    environment:
      name: staging
      url: https://staging.myapp.com
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to staging server
        env:
          SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
          HOST: ${{ secrets.STAGING_HOST }}
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          ssh -o StrictHostKeyChecking=no -i key.pem deploy@$HOST << 'EOF'
            cd /var/www/myapp
            git pull origin main
            npm ci --production
            npm run build
            pm2 restart myapp
          EOF

  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production server
        env:
          SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
          HOST: ${{ secrets.PROD_HOST }}
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          ssh -o StrictHostKeyChecking=no -i key.pem deploy@$HOST << 'EOF'
            cd /var/www/myapp
            git pull origin main
            npm ci --production
            npm run build
            pm2 restart myapp
          EOF

Deploying Docker Images

A common pattern is building a Docker image, pushing it to a registry, and then deploying the image:

name: Docker Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  deploy:
    runs-on: ubuntu-latest
    needs: build-and-push
    environment: production
    steps:
      - name: Deploy new image
        env:
          SSH_KEY: ${{ secrets.SSH_KEY }}
          HOST: ${{ secrets.HOST }}
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          ssh -o StrictHostKeyChecking=no -i key.pem deploy@$HOST << EOF
            docker pull $IMAGE
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 3000:3000 $IMAGE
          EOF

Deploying to Vercel

For platforms like Vercel, deployment is even simpler:

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    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"

Deploying to AWS S3 + CloudFront

For static sites:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci && npm run build

      - 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: Sync to S3
        run: aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} --delete

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
            --paths "/*"

Release-Based Deployments

Deploy only when you create a tagged release:

name: Release

on:
  push:
    tags:
      - "v*"

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Get version from tag
        id: version
        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Build and deploy version ${{ steps.version.outputs.version }}
        run: |
          echo "Deploying version ${{ steps.version.outputs.version }}"
          # Your deployment commands here

Rollback Strategy

Always have a rollback plan. One approach is to keep the previous version's Docker image tag:

steps:
  - name: Deploy with rollback support
    env:
      NEW_IMAGE: myapp:${{ github.sha }}
      PREV_IMAGE: myapp:previous
    run: |
      # Tag current as previous (for rollback)
      docker tag $(docker inspect --format='{{.Image}}' myapp) $PREV_IMAGE || true

      # Deploy new version
      docker stop myapp || true
      docker rm myapp || true
      docker run -d --name myapp -p 3000:3000 $NEW_IMAGE

      # Health check
      sleep 10
      if ! curl -f http://localhost:3000/health; then
        echo "Health check failed! Rolling back..."
        docker stop myapp
        docker rm myapp
        docker run -d --name myapp -p 3000:3000 $PREV_IMAGE
        exit 1
      fi

Deployment Notifications

Notify your team when deployments happen:

steps:
  - name: Notify Slack on success
    if: success()
    uses: slackapi/slack-github-action@v1
    with:
      payload: |
        {
          "text": "Deployed ${{ github.repository }} to production by ${{ github.actor }}"
        }
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

  - name: Notify on failure
    if: failure()
    uses: slackapi/slack-github-action@v1
    with:
      payload: |
        {
          "text": "Deployment FAILED for ${{ github.repository }}. Check: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
        }
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Summary

You built deployment pipelines that ship code from CI to staging and production. You learned how to use GitHub environments for approvals, deploy Docker images, integrate with cloud platforms, implement rollback strategies, and notify your team. In the next lesson, you will explore advanced workflow patterns.