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
EOFDeploying 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
EOFDeploying 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 hereRollback 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
fiDeployment 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.