Image CI Basics
This section covers the minimum viable image pipeline: build and test on every pull request, and push a digest-addressed image to the registry only from protected branches or tags.
Search across all documentation pages
This section covers the minimum viable image pipeline: build and test on every pull request, and push a digest-addressed image to the registry only from protected branches or tags.
Quick check that BuildKit is active:
docker buildx version
docker build --help | grep -i buildkitOrder instructions from least to most frequently changing so the dependency layer stays cached.
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildCOPY package*.json then npm ci split means source edits do not reinstall dependencies.node:22-slim is smaller than the full image; pin to a digest later for reproducibility.npm ci uses the lockfile for deterministic installs, unlike npm install.WORKDIR creates and enters the directory in one step.Keep build tools out of the runtime image and drop root.
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080--from=build copies only the compiled output, leaving node_modules behind.USER nginx runs as an unprivileged user, satisfying restricted Pod Security Standards.Reproduce the CI build on your machine first.
docker build -t myapp:dev .
docker run --rm -p 8080:8080 myapp:dev--rm cleans up the container after it exits.The core rule: validate on PRs, never push from them.
name: image-ci
on:
pull_request:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: false
tags: myapp:cipush: false means PR builds verify the Dockerfile without polluting the registry.setup-buildx-action provisions a BuildKit builder with cache support.main and v* tags for the publish path.Gate the push on the event so forks and PRs cannot publish.
- uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: ghcr.io/acme/myapp:${{ github.sha }}push is true only for push events, so pull requests stay build-only.github.sha gives every merge a unique, traceable image.Prefer short-lived tokens over long-lived secrets.
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}GITHUB_TOKEN is scoped to the workflow run and expires when it ends.packages: write permission only on the publish path.Capture the immutable reference the deploy will use.
docker buildx imagetools inspect ghcr.io/acme/myapp:$(git rev-parse HEAD)Digest: sha256:..., the value to deploy.Fail the build on high or critical CVEs before publishing.
- uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: myapp:ci
exit-code: "1"
severity: HIGH,CRITICALexit-code: "1" makes findings fail the job rather than warn.HIGH,CRITICAL avoids blocking on unactionable low-severity noise.Reuse layers across ephemeral runners with a registry-backed cache.
- uses: docker/build-push-action@v6
with:
context: .
push: false
tags: myapp:ci
cache-from: type=gha
cache-to: type=gha,mode=maxtype=gha uses GitHub's cache backend so cold runners restore prior layers.mode=max exports intermediate stage layers, not just the final one.Attach both a semantic tag and the commit SHA on tag builds.
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/acme/myapp
tags: |
type=semver,pattern={{version}}
type=shametadata-action derives tags and OCI labels from the git ref automatically.v1.4.0 tag produces 1.4.0 plus a sha-<short> tag for traceability.Exclude files that should never enter a layer.
## file: .dockerignore
.git
node_modules
*.env
Dockerfilenode_modules avoids shipping host-built binaries that may not match the image.*.env prevents secrets from leaking into a cached layer.Build once, then move the identical image through staging and production.
DIGEST=$(docker buildx imagetools inspect \
ghcr.io/acme/myapp:$(git rev-parse HEAD) --format '{{.Manifest.Digest}}')
echo "deploy ghcr.io/acme/myapp@$DIGEST"Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026