After this lesson, you will be able to: Build a CD pipeline that deploys to staging on PR merge and to production on release tag. Use Vercel for web apps + a VPS or Render for containers.
CD turns 'image pushed' into 'live at the URL'. This lesson wires the second half.
Different triggers for different envs: - Push to `main` → deploy staging automatically. - Create a git tag `v1.2.3` → deploy production. - GitHub Release (or workflow_dispatch with input) → deploy production with optional approval gate. Avoid: deploy to prod on every push to main. Too easy to ship a regression at 3am.
Most Next.js / static sites land here.
# .github/workflows/deploy-vercel.ymlname: Deploy (Vercel)on:push:branches: [main]workflow_dispatch:inputs:target:type: choiceoptions: [staging, production]default: stagingjobs:deploy:runs-on: ubuntu-latestenvironment:name: ${{ inputs.target || 'staging' }}steps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with: { node-version: 20, cache: npm }- run: npm ci- name: Install Vercel CLIrun: npm i -g vercel@latest- name: Pull Vercel envrun: vercel pull --yes --environment=${{ inputs.target || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}- name: Buildrun: vercel build ${{ inputs.target == 'production' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}- name: Deployrun: vercel deploy ${{ inputs.target == 'production' && '--prod' || '' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
When you control the host (do-linux-passion box). Simple, transparent, no vendor.
# .github/workflows/deploy-vps.ymlname: Deploy (VPS)on:push:branches: [main]jobs:deploy:runs-on: ubuntu-latestenvironment: productionsteps:- uses: actions/checkout@v4- name: SSH deployuses: appleboy/ssh-action@v1with:host: ${{ secrets.SSH_HOST }}username: deploykey: ${{ secrets.SSH_KEY }}script: |cd /srv/myappdocker compose pulldocker compose up -d# health checksleep 5curl -fsS http://localhost/health || exit 1
Render auto-deploys from your repo or via API.
# Render Native auto-deploy: connect repo in Render dashboard; every push to main triggers deploy.# For more control via workflow:name: Deploy (Render)on:workflow_dispatch:release:types: [published]jobs:deploy:runs-on: ubuntu-latestenvironment: productionsteps:- name: Trigger Render deployrun: |curl -X POST "https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys" \-H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" \-H "Content-Type: application/json"
GitHub Environments add per-env protections: required reviewers, wait timers, env-scoped secrets. Settings → Environments → New environment → 'production'. Add 'Required reviewers' = 1. Workflows with `environment: production` pause for approval before running. Click 'Approve' in the Actions tab. Use this for production deploys; staging is auto.
Deploying without a smoke test. The pipeline 'succeeds' but the site 404s. Always curl /health after deploy. Skipping the rollback story. If deploy fails, what's the one-liner to roll back? `git revert` + redeploy is a fine default. Deploying to prod without an approval gate. Single keystroke = global breakage. Storing deploy keys in plain repo files. Always GitHub Secrets. No deploy notifications. Slack / Discord webhook on success/failure is half the on-call story.
Pick the missing piece.
Sign in and purchase access to unlock this lesson.