Docker in CI and publishing images
Building in a pipeline, caching layers across runs, tagging with the commit, scanning for vulnerabilities and signing what you ship.
A pipeline that builds and pushes
name: image
on:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # for keyless signing
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=sha-
type=ref,event=branch
type=semver,pattern={{version}}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true- The commit SHA tag is the one you deploy: it is unique, traceable and impossible to confuse with another build.
- Cache from
type=gha(or a registry cache) is what makes a pipeline fast; without it every run rebuilds from the base image. - Build once, promote the same digest through environments. Rebuilding per environment reintroduces the risk CI was meant to remove.
- Give the job only the permissions it needs;
packages: writeis enough to push an image, and nothing should needcontents: writehere.
Tag strategy
| Tag | Example | Purpose |
|---|---|---|
| Commit SHA | sha-9f3c1ab | Immutable reference for a specific build; deploy this |
| Semantic version | 1.4.2 | Human release identity from a git tag |
| Moving alias | 1.4, 1 | Consumers who want patch upgrades automatically |
| Environment | staging, production | Convenience pointer, must be a digest under the hood |
latest | latest | Avoid in automated deploys; it invites drift |
| Digest | @sha256:... | The only exact, immutable reference |
docker buildx build --push \
--tag ghcr.io/acme/api:sha-$GIT_SHA \
--tag ghcr.io/acme/api:1.4.2 \
--tag ghcr.io/acme/api:1.4 \
--cache-from type=registry,ref=ghcr.io/acme/api:buildcache \
--cache-to type=registry,ref=ghcr.io/acme/api:buildcache,mode=max .
# record what you shipped
docker buildx imagetools inspect ghcr.io/acme/api:1.4.2 --format '{{json .Manifest.Digest}}'- Publish several tags from one build rather than building several times — same layers, different pointers.
- Never rebuild to "fix" a tag. Publish a new immutable tag and change the pointer deliberately.
- Keep the last N build-cache tags; an unbounded cache tag grows forever in the registry.
Scanning, SBOMs and signing
# fail the build on fixable high and critical findings
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed ghcr.io/acme/api:sha-$GIT_SHA
# a software bill of materials, attached as an attestation
docker buildx build --sbom=true --provenance=true --push -t ghcr.io/acme/api:sha-$GIT_SHA .
syft ghcr.io/acme/api:sha-$GIT_SHA -o spdx-json > sbom.json
# sign the digest, keyless in CI, then verify before deploy
cosign sign --yes ghcr.io/acme/api@sha256:8f1c9a4e
cosign verify ghcr.io/acme/api@sha256:8f1c9a4e \
--certificate-identity-regexp 'https://github.com/acme/api/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
grype ghcr.io/acme/api:sha-$GIT_SHA --fail-on high💡
Most findings come from the base image and the OS packages inside it, not from your code, so rebuild on a schedule even when your application has not changed. A weekly rebuild on the same commit closes vulnerabilities that appeared after you shipped.
FAQ
Should the pipeline build multi-architecture images on every commit?
Usually not. Build the architecture you deploy on every commit for fast feedback, and add the second architecture nightly or on release, ideally on a native builder rather than under emulation.
How do I keep CI builds fast?
Use BuildKit with an external cache, order the Dockerfile so dependencies are cached above the source copy, keep the context small with
.dockerignore, and use a runner with a warm local cache when the registry round trip dominates.Related
Registries, tags and image distribution BuildKit, cache and multi-platform images
Last refreshed 2026-09-18.