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.yamlLayers 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:
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:
| Stage | Purpose |
|---|---|
base | Inherits from BASE_IMAGE (shared base node image) |
dev | Minimal dev target for Docker Compose (local development) |
pruner | turbo prune --docker to isolate app dependencies |
installer | Install production dependencies from pruned lockfile |
builder | Copy source code and build the application |
cdn-assets | (Web only) Extract static assets for CDN upload |
runner | Minimal 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:
- Pruner stage -- runs
turbo prune <app> --docker, outputsout/json/(lockfile + manifests) andout/full/(source code) - Installer stage -- copies only
out/json/and runspnpm install, creating a cached dependency layer - 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:
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:
FROM scratch AS cdn-assets
COPY --from=builder /build/apps/web/.next/static /static
COPY --from=builder /build/apps/web/public /publicDuring the web build job, if pushContainerToRegistry is true, the pipeline:
- Runs a separate
docker buildx build --target cdn-assets --output type=local,dest=cdn-assetsto extract the files - Uploads
/staticto Azure Blob Storage atweb-<site>/_next/static - Uploads
/publicto Azure Blob Storage atweb-<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-23870ormain-23870-tstfor 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).