Skip to content

Docker Builds

All containerized applications are built using Docker Buildx with BuildKit. The shared build step template cicd/templates/steps/build-container.yaml handles image building, caching, and pushing to Azure Container Registry (ACR).

Build Flow

Caching Strategy

Docker builds use a two-tier caching approach:

Local Cache (Pipeline Cache)

The Azure DevOps Cache@2 task persists Docker build layers between pipeline runs using a key based on the container name and pnpm-lock.yaml:

docker-buildx | "$(Agent.OS)" | <containerName> | pnpm-lock.yaml

Layers are stored with mode=max and compression=zstd for fast transfer. After each build, the old cache is replaced with the new one to keep it fresh.

Registry Cache (ACR)

Each container also uses a registry-based cache stored as <containerName>:buildcache in ACR. This acts as a fallback when the local cache has no match (e.g., first run on a new agent).

The build command combines both sources:

bash
docker buildx build \
  --cache-from type=local,src=$CACHE_DIR \
  --cache-from type=registry,ref=<registry>/<name>:buildcache \
  --cache-to type=local,dest=$CACHE_DIR_NEW,mode=max,compression=zstd \
  --push \
  .

Local cache is checked first (faster), registry cache second.

Multi-Stage Dockerfiles

App Dockerfiles use a consistent multi-stage pattern:

StagePurpose
baseInherits from BASE_IMAGE (shared base node image)
devMinimal dev target for Docker Compose (local development)
prunerturbo prune --docker to isolate app dependencies
installerInstall production dependencies from pruned lockfile
builderCopy source code and build the application
cdn-assets(Web only) Extract static assets for CDN upload
runnerMinimal production image (Alpine, non-root user)

The CI/CD pipeline always targets the runner stage (--target runner).

Turbo Prune

All app Dockerfiles use turbo prune --docker to create an optimized Docker build context. This separates dependency manifests from source code, enabling better layer caching:

  1. Pruner stage -- runs turbo prune <app> --docker, outputs out/json/ (lockfile + manifests) and out/full/ (source code)
  2. Installer stage -- copies only out/json/ and runs pnpm install, creating a cached dependency layer
  3. Builder stage -- copies out/full/ (source code) and builds, only invalidating cache when source changes

Turbo Remote Cache

Builds inject TURBO_TOKEN as a Docker secret (--secret id=TURBO_TOKEN,env=TURBO_TOKEN). Inside the Dockerfile, the build step mounts this secret to enable Turbo remote cache reads and writes:

dockerfile
RUN --mount=type=secret,id=TURBO_TOKEN \
    TURBO_TOKEN=$(cat /run/secrets/TURBO_TOKEN) \
    pnpm run build --filter=<app>

This means build artifacts are shared between PR and release pipeline runs.

CDN Asset Extraction (Web)

The web Dockerfile includes a cdn-assets stage that extracts static files for CDN hosting:

dockerfile
FROM scratch AS cdn-assets
COPY --from=builder /build/apps/web/.next/static /static
COPY --from=builder /build/apps/web/public /public

During the web build job, if pushContainerToRegistry is true, the pipeline:

  1. Runs a separate docker buildx build --target cdn-assets --output type=local,dest=cdn-assets to extract the files
  2. Uploads /static to Azure Blob Storage at web-<site>/_next/static
  3. Uploads /public to Azure Blob Storage at web-<site>/public

These assets are served via Cloudflare CDN (cdn.feyenoord.com, cdn-tst.feyenoord.com, cdn-acc.feyenoord.com). Each environment has its own storage container, and Next.js generates content-hashed filenames so assets from multiple deployments coexist safely.

Conditional Push

When pushContainerToRegistry is false (e.g., on release branches), the build uses --output type=cacheonly. This validates the build and updates the cache without pushing an image to ACR.

Image Tagging

Images are tagged with two tags:

  • Version tag: <branch-or-tag>-<BuildId> (e.g., main-23870 or main-23870-tst for web)
  • Latest tag: latest

Web images include the environment suffix because each environment produces a different build (different env vars baked in at build time).