Chapter 9: CI/CD Integration and Registries


All the techniques in this book deliver full value only when they are applied consistently — not just on a developer’s machine, but on every build in your CI/CD pipeline. This chapter covers how to wire up BuildKit caching in CI, how to enforce image size and security gates, how to choose a registry, and how to build for multiple architectures.


GitHub Actions: The Reference Workflow

GitHub Actions is used here as the reference CI platform because it has the best native Docker support. The patterns translate directly to GitLab CI, CircleCI, and Jenkins — the key flags and commands are identical; only the YAML syntax differs.

Core workflow structure

name: Build and push Docker image

on:
  push:
    branches: [main]
  pull_request:

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

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: $
          password: $

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: $
          tags: ghcr.io/$:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Full workflow: code/github_actions_workflow.yml — includes lint, size gate, and CVE scan steps.

Cache strategy explained

cache-from: type=gha reads from the GitHub Actions cache. cache-to: type=gha,mode=max writes all layers back, including intermediate stages from multi-stage builds. mode=max is essential for multi-stage builds — without it, only the final stage’s layers are cached, and the builder stage is re-run on every build.

On a warm cache, a build that previously took 4 minutes typically completes in 30–60 seconds.


Size Gate

Enforce a maximum image size as a CI check to prevent size regressions:

- name: Check image size
  run: python code/check_image_size.py myapp:latest 209715200

209715200 is 200 MB in bytes. check_image_size.py exits non-zero if the image exceeds the threshold, failing the CI job.

Script: code/check_image_size.py

Alternatively, use dive --ci:

- name: Check layer efficiency
  run: |
    docker run --rm \
      -v /var/run/docker.sock:/var/run/docker.sock \
      wagoodman/dive:latest --ci myapp:latest

This fails if the image efficiency score is below the threshold configured in .dive-ci.yml.


Dockerfile Linting Gate

Run hadolint before building, to catch Dockerfile mistakes early:

- name: Lint Dockerfile
  uses: hadolint/hadolint-action@v3.1.0
  with:
    dockerfile: Dockerfile

This action exits non-zero on any finding and posts inline annotations on pull requests.


CVE Scan Gate

- name: Scan for vulnerabilities
  uses: docker/scout-action@v1
  with:
    command: cves
    image: myapp:latest
    only-severities: critical,high
    exit-code: true

exit-code: true fails the job if any critical or high CVEs are found. This creates a strong incentive to keep the base image updated and packages minimal.


Image Tagging Strategy

A consistent tagging strategy makes images traceable and avoids the pitfalls of relying on latest.

tags: |
  ghcr.io/$:latest
  ghcr.io/$:sha-$
Tag Purpose
sha-<commit> Unique, immutable, fully traceable to a commit
latest Convenience alias; useful for pull-based deploys
v1.2.3 Semver release tag; set on release events
main Current state of the main branch

Never rely on latest alone in production deployments. Use the sha-<commit> tag in Kubernetes manifests or Helm values so rollbacks are unambiguous.


Registry Comparison

Registry Free tier Storage pricing Egress pricing Notes
Docker Hub 1 private repo, rate-limited pulls $5/mo (Pro) Free Rate limits affect CI pulls
GitHub Container Registry (ghcr.io) Free for public repos $0.008/GB/month Free within GitHub Best for GitHub-hosted projects
AWS ECR 500 MB/month free $0.10/GB/month $0.09/GB (internet) Best for AWS deployments
Google Artifact Registry 0.5 GB free $0.10/GB/month $0.08/GB Best for GCP deployments
GitLab Container Registry Included in GitLab Included Included Best for GitLab-hosted projects

For most projects, ghcr.io is the lowest-friction choice: free, no rate limits within GitHub Actions, and no separate account required.

Cost calculation example: A 100 MB image pushed 10 times/day from 5 nodes costs roughly:

The egress cost dominates — reducing image size from 500 MB to 100 MB reduces this to $2.40/month. At scale, with many services and many nodes, the savings compound.


Multi-Architecture Builds

Teams deploying to AWS Graviton (arm64), Apple Silicon dev machines, or Raspberry Pi need images that run on both linux/amd64 and linux/arm64. BuildKit’s buildx handles this:

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3

- name: Build and push (multi-arch)
  uses: docker/build-push-action@v6
  with:
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ghcr.io/$:latest

setup-qemu-action installs QEMU for software emulation of the non-native architecture. The build-push-action builds both platform variants in parallel and pushes a multi-platform manifest that Docker automatically resolves to the correct architecture at pull time.

Note: QEMU emulation is slow. For fast CI builds, use native runners for each architecture and merge manifests with docker manifest.


Key Takeaways


← Chapter 8: Analyzing Images Table of Contents Chapter 10: Action Plan →