Building Docker images on your laptop and pushing them by hand doesn’t scale, and isn’t reproducible. A GitHub Actions workflow can build an image on every push, tag it properly, push it to a registry, and trigger a deployment to your VPS. Here’s a complete, production-ready setup, using GitHub Container Registry (GHCR).

The workflow

# .github/workflows/docker.yml
name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*.*.*"]
  pull_request:

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Tags and labels
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,format=short

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

What it does:

  • Pull requests: build only (to check that the Dockerfile works), with no push.
  • Pushes to main: push ghcr.io/owner/repo:main and :sha-a1b2c3d.
  • Tags like v1.8.3: push :1.8.3 and :1.8.
  • GITHUB_TOKEN with packages: write is enough for GHCR. No personal token needed.
  • type=gha caching makes rebuilds much faster. See Docker build cache.

Tagging strategy: Docker image tags best practices.

Multi-architecture images

Add QEMU, and a platforms list, to build amd64 and arm64 images:

      - uses: docker/setup-qemu-action@v3
      # …
      - uses: docker/build-push-action@v6
        with:
          platforms: linux/amd64,linux/arm64
          # … other settings as above

See multi-arch images with buildx.

Scan before pushing

      - name: Scan image
        uses: aquasecurity/[email protected]
        with:
          image-ref: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
          severity: CRITICAL,HIGH
          ignore-unfixed: true
          exit-code: "1"

(Build with load: true, or scan after the push, depending on your flow.) See Trivy image scanning.

Pulling on your VPS

GHCR packages are private by default for private repositories. On the server, log in with a personal access token (classic, with read:packages), or a fine-grained token:

echo "$GHCR_TOKEN" | docker login ghcr.io -u <github-user> --password-stdin
docker pull ghcr.io/owner/repo:1.8.3

Or make the package public in its settings, for open-source projects.

Deploy after the build

Add a job that runs after build, connects to the VPS over SSH, and updates the stack:

  deploy:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /opt/app
            TAG="${{ github.ref_name }}"; TAG="${TAG#v}"
            sed -i "s/^APP_IMAGE_TAG=.*/APP_IMAGE_TAG=$TAG/" .env
            docker compose pull && docker compose up -d

See GitHub Actions deploy to VPS, GitHub Actions secrets, and Ansible Docker deploy.

Self-hosted runners

For faster builds with persistent caches, or builds that need access to private networks, run a self-hosted runner on your own VPS (private repositories only). See self-hosted GitHub runner.

Quick reference

Step Action
Builder docker/setup-buildx-action
Login docker/login-action + GITHUB_TOKEN
Tags docker/metadata-action (branch, semver, sha)
Build/push docker/build-push-action with cache-from/to: gha
Multi-arch setup-qemu-action + platforms:
Permissions packages: write

Related: Dockerfile tutorial and CI/CD pipeline explained.

  • GitHub Actions
  • Docker
  • CI/CD