Docker in CI/CD
Docker in CI/CD: building images, multi-stage builds, layer caching, registries, and image promotion.
Docker — CI/CD
EXAMPLE
# ===== Multi-stage Dockerfile =====
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER nonroot
CMD ["dist/server.js"]
# Final image: tiny, no shell, non-root.
# ===== GitHub Actions =====
name: docker
on:
push: { branches: [main] }
jobs:
build:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
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 }}
- uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ===== Layer caching =====
# Order Dockerfile lines from LEAST to MOST frequently changing:
# 1. FROM (rarely)
# 2. System deps (rarely)
# 3. package.json (sometimes)
# 4. npm ci (when package.json changes)
# 5. App code (often)
# This maximises cache hits during builds.
# ===== Registries =====
# Docker Hub public + paid private
# GitHub GHCR free for public + tied to repo perms
# AWS ECR AWS-native; IAM integration
# Google GAR GCP-native
# Quay (Red Hat) enterprise
# ===== Image promotion =====
# Build once; tag through environments:
docker tag shop:sha-abc shop:dev
docker tag shop:sha-abc shop:staging
docker tag shop:sha-abc shop:prod
# Never rebuild for prod from main — same artifact through all environments.
# ===== Security =====
# - Trivy / Grype scan in CI; fail on HIGH+
# - Sign images with cosign + Sigstore
# - Pin base by digest in production
# - Distroless / scratch / alpine where possible
# - Non-root user
# - SBOM (Syft) attached to each image
# ===== Image tags =====
# Avoid 'latest' in production. Recommended tag scheme:
# - <git-sha> immutable build artifact
# - <semver> for releases (v1.2.3)
# - <branch-name> for dev / preview
# - <env>-latest moving pointer per environment
# ===== Patterns =====
# - Multi-stage builds; tiny runtime image
# - Layer cache friendly Dockerfile ordering
# - Build once, promote artifact through environments
# - Sign + scan + SBOM in pipeline
# - Pin base by digest in prod
# ===== Pitfalls =====
# - 'latest' tag in prod -> non-reproducible deploys
# - Rebuilding for each environment (drift)
# - Root user inside container
# - Huge final image (forgot to use multi-stage)
Why it matters
Docker in CI: multi-stage build, layer cache, tagged push to a registry, scan + sign in the pipeline. Promote ONE artifact through environments by tag; never rebuild for prod. Pin base by digest, non-root user, distroless runtime — the production image gets safe + tiny.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# GitHub Actions snippet
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/user/my-api:${{ github.sha }}
Try it Yourself »
Discussion
Loading…