Back to the curriculum
Part 4 · Lesson 09
Production-Grade

CI/CD with GitHub Actions

Why this beats "git pull && restart" for any team larger than one.

intermediate14 min readUpdated 2026-04-11

Why not just `git pull`

git pull && docker compose up -d --build works fine for one developer on one project. The moment you have two people, or the build takes more than 60 seconds, or you need to roll back fast, you want the artifact (the built image) created once, tagged, and pulled on the server — not rebuilt in place. CI/CD is just "build once, deploy artifacts".

GHCR: free Docker registry for every repo

GitHub Container Registry gives every repo a free private Docker registry at ghcr.io/<owner>/<repo>. No separate signup, uses your GitHub token, and images are free as long as your repo is under the GitHub free tier storage limit (which is generous). Push to GHCR from CI, pull on the VPS. No DockerHub account needed.

The smallest working workflow

name: deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ github.sha }}
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /srv/app
            docker compose pull
            docker compose up -d

Two secrets (SSH_HOST, SSH_KEY), one workflow file, a full deploy pipeline.

Tag every build with the commit SHA

Tagging images with :latest and :${{ github.sha }} is the rollback mechanism. When something breaks in production, you set image: ghcr.io/owner/repo:abc123 in compose.yml to the previous SHA and docker compose up -d — 10 seconds to roll back. Without SHA tags, you are rebuilding from the previous commit, which is both slower and more error-prone.

Secrets belong in GitHub, not the repo

Anything sensitive (DATABASE_URL, API keys, SSH keys) lives in GitHub repo secrets — settings → secrets and variables → actions. The workflow references them as ${{ secrets.NAME }}; they never appear in logs, never land in git history, and are scoped to the repo. The two secrets you always need: SSH_HOST and SSH_KEY for the deploy step.

Key takeaways

  • Build once in CI, pull artifacts on the server — not `git pull`
  • GHCR is a free private Docker registry for every repo
  • Tag every image with `:latest` and `:$SHA` for fast rollbacks
  • Secrets live in GitHub, never in the repo or the workflow YAML

Related documentation