Docker Build GitHub Actions Workflow Template
Last updated
This is a working .github/workflows/ci.yml that builds your Docker image on every pull request and pushes it to GitHub Container Registry (GHCR) from main. It uses Buildx with the GitHub Actions layer cache so unchanged layers are not rebuilt.
The workflow
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
IMAGE: ghcr.io/${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- 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 the non-obvious parts do
- Permissions. The workflow default is read-only. The job adds
packages: writebecause only it pushes images. Pull requests from forks get a read-only token anyway, so they cannot push. setup-buildx-actioncreates a Buildx builder. The default Docker driver cannot export the GitHub cache, so this step is required forcache-to: type=gha.- Login only when pushing. On pull requests there is nothing to push, so the step is skipped.
docker/metadata-actiongenerates tags and OCI labels. The example tags every build with the commit SHA and addslateston the default branch.push:expression. Pull requests build only, as a check that the Dockerfile still works. Pushes tomainbuild and push.cache-fromandcache-to: type=gha,mode=maxread and write the GitHub Actions cache.mode=maxcaches every layer from every stage, which matters for multi-stage builds, though it uses more cache space. See cache limits.- Dockerfile order matters more than any setting. Copy lockfiles and install dependencies before copying source, so code edits do not invalidate dependency layers. The full list of techniques is in Docker layer caching in CI.
Variants
Multi-platform images
Add docker/setup-qemu-action@v3 before Buildx and platforms: linux/amd64,linux/arm64 to the build step. Emulated arm64 builds are slow; use a native ARM runner when speed matters.
Scan before push
Add a vulnerability scan step on the built image before the push, and fail on high severity findings.
Run tests inside the image
Add a first build step with load: true, push: false and a test stage, then run docker run on the result. Share layers through the same cache.
Docker Hub or ECR
Change registry, the login step and images. For AWS, use OIDC with aws-actions/amazon-ecr-login@v2 instead of stored keys.
Deploy job
Add a second job with needs: build that deploys the pushed SHA tag.
macOS
Docker is not available on macOS runners. Keep container builds on Linux.
Run it on faster machines
Docker builds are CPU and disk heavy, and layer caches only help if they are close to the build. Change one line:
runs-on: [self-hosted, Linux, compiler-ci]
The job runs on a fresh 8 vCPU machine from compiler.dev, which keeps a Docker layer cache on the machine, with automatic fallback to your original runner. For image builds specifically, dedicated builders such as Depot can be faster; compare options in faster GitHub Actions runners. Prices are on pricing.
FAQ
Why is my build not using the cache?
Check that Buildx is set up, that cache-from and cache-to match, and that an early Dockerfile layer is not changing every run (for example copying the whole repo first).
Is the GitHub cache big enough?
Repositories have a size cap shared by all caches, and old entries are evicted. Large images can fill it.
Do pull requests from forks push images?
No. The push expression is false for pull requests, and fork tokens are read-only.
What are artifacts for, then?
Use an artifact to pass a built tarball between jobs, not for layer caching.
Related
- Docker Build Slow in CI? Find the Cause and Fix ItDocker build slow in CI? Diagnose build context size, .dockerignore, layer order, cache misses and BuildKit settings, then fix the biggest one first.
- Docker Layer Caching in CI With BuildxMake Docker builds in GitHub Actions fast with buildx cache-from and cache-to, the gha and registry backends, and a Dockerfile ordered for cache hits.
- Docker Pull Rate Limit in GitHub Actions: FixesFix "toomanyrequests: You have reached your pull rate limit" in GitHub Actions: Docker Hub limits, docker/login-action, mirrors and GHCR alternatives.
- .NET GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for .NET with NuGet caching, restore, build and test steps, a SQL Server service option, concurrency and permissions.