Skip to main content

Build-Once, Promote-Everywhere

What This Concept Is

A single immutable artifact is built from a given commit once, tested in that form, and then promoted -- unchanged -- through every environment on its way to production. Configuration varies per environment; the artifact does not.

The slogan has three parts:

  1. Build once. One source commit, one build, one tagged artifact (e.g. app:a1b2c3d).
  2. Promote everywhere. The same bytes go to dev -> staging -> production.
  3. Configure at runtime. Environment differences (database URLs, feature flags, secrets) are injected as configuration, not compiled in.

The alternative -- rebuilding the artifact at each environment -- means "the thing we tested" is not "the thing we shipped." You gave up reproducibility at the first rebuild. This is why The Twelve-Factor App devotes a full factor (III -- Config) to the separation of config from code: config must live outside the artifact and vary per environment, while the artifact is byte-identical across all of them.

Why It Matters Here

This is the single most important CI invariant:

  • it lets you compare environments honestly (dev and prod run the same bits)
  • it makes rollback trivial (redeploy the previous tagged artifact)
  • it makes incident investigation tractable (the production binary is exactly the one in the artifact store)
  • it eliminates a whole class of "works in staging, broken in prod" bugs caused by divergent builds
  • it is the precondition for supply-chain security -- you cannot meaningfully sign or attest an artifact you rebuild per environment (see concept 11)
  • it is the precondition for SemVer (concept 10) -- the v1.2.3 tag points to exactly one set of bits, not "whatever came out of the staging build that day"

Without this invariant, the rest of the pipeline is speculation dressed up as engineering.

Concrete Example

A correct flow for a containerized service:

name: Build and promote
on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- id: meta
run: echo "image=ghcr.io/acme/api:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.image }}

deploy-staging:
needs: build
environment: staging
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh --image "${{ needs.build.outputs.image }}" --env staging

deploy-prod:
needs: deploy-staging
environment: production
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh --image "${{ needs.build.outputs.image }}" --env production

Note what is not there: no docker build step in the deploy jobs. The image built once on main is the only image the deploy jobs can reference. The commit SHA -- the same identity Pro Git emphasises as the cryptographic name of a commit -- becomes the artifact tag, so a given digest is provably derived from a specific source tree.

Configuration at Runtime -- a Layered Source

Configuration enters the artifact only at process start. A typical layering, from most general to most specific:

  1. Compiled defaults -- safe fallbacks baked into the binary (feature flags off, conservative timeouts).
  2. Environment variables -- populated by the deployer from a secret store or environment config.
  3. Mounted config files -- Kubernetes ConfigMaps / Secrets, cloud Parameter Store / SSM.
  4. Runtime feature-flag service -- last-mile overrides (see concept 8).

Shell fluency matters here: deploy scripts typically read environment variables, substitute them into templates, and exec the binary. The Linux Command Line chunks on environment variables and here-documents are the baseline skill: knowing what is stored in the environment and how processes inherit it is the difference between "works on my laptop" and "works in every environment."

Common Confusion / Misconception

"We rebuild per environment to use environment-specific compile flags." If you have genuinely different binaries per environment, you have multiple products, not one. More often, "environment-specific compile flags" are configuration masquerading as build flags -- move them to runtime.

"A new build for every environment is safer because it's fresher." The opposite. A fresh build might pick up a new base-image digest, a new transitive dependency, or a new compiler. You have silently swapped bits between staging validation and the prod deploy.

"We tag the image as staging and prod separately." That is still one image if they are both tags on the same digest. The anti-pattern is re-building, not re-tagging. For releases you want immutable tags too: once v1.2.3 is pushed, the registry must refuse overwrites. GHCR, ECR, and GCR all support this.

"Configuration in the image is fine for a small app." It is, until the day you need to rotate a secret without a rebuild, or promote an artifact that you no longer have the source tree for. Extract configuration early; the cost of doing so later is much higher.

"Tags are the identity." They are not. An OCI image tag is a mutable pointer to a digest; the digest (sha256:...) is the content-addressable identity. Deploy by digest in manifests, use tags only as human-readable aliases. The same discipline Pro Git teaches for git -- commit SHAs are identity, branch names are pointers -- applies to artifacts.

How To Use It

Treat the pipeline as an assembly line:

  1. The build stage is the only stage that produces artifacts.
  2. Each later stage consumes the artifact by immutable reference -- image digest (sha256:...), not a mutable tag.
  3. Configuration is loaded at runtime from a source the artifact cannot influence (cloud secrets manager, env vars injected by the deployer, ConfigMaps).
  4. Rollback = redeploy previous digest. No rebuild, ever, for rollback.
  5. Retention policy in the registry keeps at least N previous digests addressable for rollback. "We garbage-collected the rollback image" is a story you only want to tell once.

Check Yourself

  1. Why does rebuilding per environment invalidate "the thing we tested"?
  2. What is the difference between promoting an image by tag versus by digest?
  3. Where do secrets come from, if not from the artifact?
  4. What is the one-step rollback from a bad production artifact?
  5. What is the relationship between "build-once" and the concept of an immutable tag in the registry?

Mini Drill or Application

Audit a pipeline you have access to. Answer:

  • How many docker build / npm run build / go build steps are in the full deploy path?
  • If the answer is more than one, which stage genuinely needs to rebuild and why?
  • Do deploy stages reference artifacts by mutable tag or immutable digest?
  • Where are environment-specific values injected, and can a user change them without a rebuild?
  • What is the retention policy on the registry? How many past digests are still deployable today?

A passing pipeline has exactly one build stage and deploys reference an immutable artifact id.

Read This Only If Stuck

See also (external)