CI/CD Pipelines
Building images in CI, layer caching in pipelines, multi-platform builds, and registry tagging. You'll hit this when your CI pipeline rebuilds everything from scratch on every commit.
# GitHub Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:latest .
# Builds from scratch every time
# No layer caching# GitHub Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:latest .
# Builds from scratch every time
# No layer caching# GitHub Actions
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:latest
cache-from: type=gha
cache-to: type=gha,mode=max# GitHub Actions
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:latest
cache-from: type=gha
cache-to: type=gha,mode=maxWithout layer caching, every CI run builds every layer from scratch. Installing dependencies, compiling code, and copying assets all run again even if nothing changed. This wastes time and compute resources on every commit.
BuildKit's GitHub Actions cache backend (type=gha) stores and retrieves Docker layers between CI runs. Unchanged layers are reused, dramatically reducing build times. mode=max caches all layers, not just the final image.
# Build and push with mutable tag
docker build -t registry.io/myapp:latest .
docker push registry.io/myapp:latest
# Deploy
kubectl set image deployment/web \
web=registry.io/myapp:latest# Build and push with mutable tag
docker build -t registry.io/myapp:latest .
docker push registry.io/myapp:latest
# Deploy
kubectl set image deployment/web \
web=registry.io/myapp:latest# Build and push with immutable tag
SHA=$(git rev-parse --short HEAD)
docker build -t registry.io/myapp:$SHA .
docker push registry.io/myapp:$SHA
# Deploy with specific version
kubectl set image deployment/web \
web=registry.io/myapp:$SHA# Build and push with immutable tag
SHA=$(git rev-parse --short HEAD)
docker build -t registry.io/myapp:$SHA .
docker push registry.io/myapp:$SHA
# Deploy with specific version
kubectl set image deployment/web \
web=registry.io/myapp:$SHAlatest is a mutable tag that gets overwritten on every push. You can't tell which version is running without inspecting the image digest. Rollbacks to latest deploy whatever was last pushed, not a specific known-good version. Different environments pulling latest at different times get different code.
Tagging with the commit SHA creates an immutable, traceable image. You can always determine which code is running in any environment. Rollbacks point to a specific previous SHA. Two environments running the same SHA are guaranteed to have identical code.
# Only builds for CI runner's arch
docker build -t myapp:1.0 .
docker push registry.io/myapp:1.0
# Fails on ARM servers or
# Apple Silicon dev machines# Only builds for CI runner's arch
docker build -t myapp:1.0 .
docker push registry.io/myapp:1.0
# Fails on ARM servers or
# Apple Silicon dev machines# Build for multiple architectures
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.io/myapp:1.0 \
--push .
# Works on x86 servers, ARM servers,
# and Apple Silicon dev machines# Build for multiple architectures
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.io/myapp:1.0 \
--push .
# Works on x86 servers, ARM servers,
# and Apple Silicon dev machinesBuilding without --platform produces an image only for the CI runner's architecture (usually amd64). Deploying this on ARM servers causes exec format errors. Developers on Apple Silicon Macs run the image through slow emulation.
docker buildx build --platform creates a multi-architecture manifest. Docker automatically pulls the right image for the host architecture. Your image works on x86 CI runners, ARM-based cloud instances (Graviton, Ampere), and Apple Silicon Macs.
# Build, push, deploy
# No security check
docker build -t myapp:$SHA .
docker push registry.io/myapp:$SHA
kubectl set image deployment/web \
web=registry.io/myapp:$SHA# Build, push, deploy
# No security check
docker build -t myapp:$SHA .
docker push registry.io/myapp:$SHA
kubectl set image deployment/web \
web=registry.io/myapp:$SHA# Build, scan, push, deploy
docker build -t myapp:$SHA .
# Scan for vulnerabilities
docker scout cves myapp:$SHA \
--exit-code \
--only-severity critical,high
# Only push and deploy if scan passes
docker push registry.io/myapp:$SHA
kubectl set image deployment/web \
web=registry.io/myapp:$SHA# Build, scan, push, deploy
docker build -t myapp:$SHA .
# Scan for vulnerabilities
docker scout cves myapp:$SHA \
--exit-code \
--only-severity critical,high
# Only push and deploy if scan passes
docker push registry.io/myapp:$SHA
kubectl set image deployment/web \
web=registry.io/myapp:$SHADeploying without scanning means known CVEs in your base image or dependencies go straight to production. By the time a periodic scan catches them, the vulnerable image has been serving traffic for hours or days.
Scanning images before pushing to a registry catches known vulnerabilities in base images and dependencies. --exit-code makes the scan fail the pipeline on critical/high findings. Vulnerable images never reach production.