Back to path
IntermediateBeacon · Project 4 of 12 ~6h· 5 milestones

Build once, promote everywhere

Continues from the last build: CI tests every change, but it rebuilds the api and worker images separately for each environment, so what your tests passed against is not byte-for-byte what reaches production.

Last rung gave you green CI on every change, and that felt like a win until the on-call pager went off.

Release engineering and the build-once principleImmutable artifact tagging by git short SHADocker Buildx with registry-backed layer cachingMulti-stage Dockerfiles and dependency-layer caching in a monorepoOCI image labels for build provenanceSeparating build from deploy in a CI/CD pipelineProving artifact identity with image digests

What you'll build

You will have a release pipeline that builds the api and worker images one time per commit, tags them immutably with the git short SHA, records provenance as OCI image labels, and pushes them to the existing registry. Promotion to any environment becomes a deploy of that exact digest, never a rebuild, so what you tested is provably what runs. Multi-stage Dockerfiles built from the repo root can see the shared libs/ package, dependency and layer caching cut repeat builds from minutes to under two minutes, and a digest-equality check proves staging and production run identical bytes.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

.github/workflows/release.ymlyaml
name: release
on:
  push:
    branches: [main]

jobs:
  staging:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build api for staging
        run: docker build -t registry.internal/beacon-api:staging ./api
      - name: Build worker for staging
        run: docker build -t registry.internal/beacon-worker:staging ./worker
      - run: docker push registry.internal/beacon-api:staging
      - run: docker push registry.internal/beacon-worker:staging

  production:
    needs: staging
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Rebuild api for production
        run: docker build -t registry.internal/beacon-api:prod ./api
      - name: Rebuild worker for production
        run: docker build -t registry.internal/beacon-worker:prod ./worker
      - run: docker push registry.internal/beacon-api:prod
      - run: docker push registry.internal/beacon-worker:prod

Reading this file

  • name: Rebuild api for productionThe smell: production builds its own image from source instead of reusing the staging artifact. Different build, different bytes.
  • docker build -t registry.internal/beacon-api:staging ./apiContext is ./api, so the build cannot COPY the sibling libs/ package. Any import of libs fails to build.
  • registry.internal/beacon-api:prodA second, independently produced image under a mutable environment tag. Nothing guarantees it equals the staging one.
  • needs: stagingOrdering exists, but it gates a rebuild, not a promotion of the tested artifact.

The current release workflow. It rebuilds the same code twice, once targeting staging and once targeting production, so the artifacts are never proven identical. It also builds from each service folder, which cannot see the shared libs/ package.

That's 1 of 7 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Compute one immutable tag per commit

    3 guided steps

    A mutable tag like :staging or :prod answers where an image is, not what it contains. An immutable :sha tag makes the image content-addressable by commit, which is the precondition for promoting one artifact instead of rebuilding.

  2. 2

    Cache dependencies and reach the shared libs in the Dockerfiles

    4 guided steps

    Layer ordering is the whole game for cache hits: if dependencies install before source is copied, an app-only change reuses the dependency layer and the build finishes in seconds. Installing the project itself with --no-deps as a separate, last step keeps the heavy dependency layer cached while still wiring up the package. Labels make a running container self-describing.

  3. 3

    Build each image exactly once with Buildx from the repo root

    4 guided steps

    Building once is the core of release engineering: the artifact pushed here is the only one that will ever run. A repo-root context gives both services access to libs/. Registry cache makes that single build cheap on every subsequent commit. Disabling provenance and SBOM attestations keeps the pushed reference a plain image rather than an index, which the digest proof in milestone 5 depends on.

  4. 4

    Promote the artifact without rebuilding

    4 guided steps

    This is where build-once pays off: promotion is a pointer change, not a new build. The bytes that passed your tests in staging are the identical bytes that reach production, eliminating the rebuild drift that paged you.

  5. 5

    Prove byte-for-byte identity across environments

    3 guided steps

    A tag is a label that could in theory be re-pushed, a digest is the content hash and cannot lie. Asserting digest equality turns build-once from a claim into a checked invariant and is the audit artifact that closes the incident that started this rung.

What's inside when you start

2 starter files, ready to clone
5 guided milestones
5 full reference solutions
7 code blocks explained line-by-line
5 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

A refactored release.yml with one setup job, one build job that builds api and worker exactly once from the repo root, and deploy jobs that promote by tag without rebuilding
Multi-stage Dockerfiles for api and worker that cache dependencies separately from source, COPY the shared libs/ package, and stamp OCI provenance labels
Registry-backed Buildx caching configured with cache-from and cache-to, with attestations disabled, so repeat builds finish under two minutes and push single-platform images
Images in the existing registry tagged immutably by git short SHA for both services
A digest-equality check that proves staging and production run byte-for-byte identical images and match the built artifact

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building