Build Once Promote the Artifact

“Build once, promote the artifact” is the load-bearing rule of a deployment pipeline: compile and package the software exactly once, then take that identical, immutable binary or image and promote it — unchanged — through every stage of the pipeline (test → staging → production). Humble and Farley state the rule as “Only build your binaries once”, and the reasoning is uncompromising: “Every time you compile the code, you run the risk of introducing some difference,” so “the binaries that get deployed into production should be exactly the same as those that went through the acceptance test process” (Humble & Farley, Anatomy of the Deployment Pipeline). The entire value of a pipeline is that a green light at the acceptance stage means something about production. Rebuilding the artifact for each environment silently throws that guarantee away — you would be testing one binary and shipping a different one. Environment-specific differences are handled not by rebuilding but by injecting configuration at deploy time, keeping the artifact constant and the config variable.

This note is about the pipeline principle and its rationale. The mechanics of how the same artifact is advanced between environments live in Artifact Promotion Across Environments; the pipeline structure that this rule sits inside is The Deployment Pipeline; the versioning discipline that makes an artifact uniquely and immutably addressable is Immutable Artifact Versioning.

Mental Model: One Build, Many Deploys

The rule is best understood as a hard architectural boundary in the pipeline. There is exactly one point where source becomes an artifact — the commit stage — and everything after it is a deployment of that same artifact, never a rebuild.

flowchart LR
    SRC["Source commit<br/>(from trunk)"] --> BUILD["Commit stage:<br/>COMPILE + PACKAGE<br/>ONCE"]
    BUILD --> ART[("Artifact<br/>sha256:abc123<br/>immutable")]
    ART -->|"deploy + inject<br/>test config"| TEST["Test env"]
    ART -->|"deploy + inject<br/>staging config"| STAGE["Staging env"]
    ART -->|"deploy + inject<br/>prod config"| PROD["Production"]

    TEST -.->|"gate: passed?"| STAGE
    STAGE -.->|"gate: passed?"| PROD

What it shows and the insight to take: a single artifact sha256:abc123 is born once at the commit stage and then fans out to every environment. The bytes never change — the only thing that differs between environments is the configuration injected at deploy time. The dashed edges are the pipeline gates: staging only receives the artifact that passed in test, and production only receives the one that passed in staging. Because it is literally the same bytes each time, “it passed in staging” is a valid statement about what will run in production. Contrast this with the anti-pattern below, where each environment gets its own build.

flowchart LR
    SRC["Source commit"] --> B1["Build for TEST"] --> T["Test env<br/>artifact-A"]
    SRC --> B2["Build for STAGING"] --> S["Staging env<br/>artifact-B"]
    SRC --> B3["Build for PROD"] --> P["Production<br/>artifact-C"]
    T -.->|"tested A"| X1["≠"]
    P -.->|"shipped C"| X1

What it shows and the insight to take: three separate builds from the same source produce three different artifacts (A, B, C) because each compilation can pick up a different compiler version, a different transitive dependency, or a different build-host state. You tested artifact-A; you shipped artifact-C. Every test result upstream is now a statement about a binary that does not exist in production. This is precisely the variance the pipeline was built to eliminate, reintroduced at the worst possible place.

Why Rebuilding Per Environment Is a Defect

Humble and Farley enumerate the concrete ways a recompile can diverge from what was tested (Humble & Farley):

  • “The version of the compiler installed in the later stages may be different.” The prod build agent has GCC 13; the test agent had GCC 12. Same source, different codegen.
  • “You may pick up a different version of some third-party library that you didn’t intend.” A dependency resolved to 1.4.2 when test ran and 1.4.3 when prod built, because a floating version range or an unpinned lockfile let it drift.
  • “The configuration of the compiler may change the behavior.” Optimization flags, feature toggles in the build, or environment variables read at compile time differ between agents.

Each of these is a channel through which the production binary can differ from the tested binary without anyone changing a line of source. The rule closes all of them at once by making the binary a fixed input to every downstream stage. As the continuousdelivery.com patterns page puts it: “We want to be sure the thing we’re deploying is the same thing we’ve tested throughout the deployment pipeline, so if a deployment fails we can eliminate the packages as the source of the failure” (continuousdelivery.com, Patterns).

That last clause names the second, subtler benefit: debuggability. When something breaks in production, “build once” lets you eliminate the artifact as a variable. If the identical bytes passed every prior stage, the failure must lie in configuration, data, or environment — not in the binary. Under per-environment rebuilds, the binary is always a suspect, and you can never fully rule it out. Build-once turns “is it the code, or the environment?” from an open question into a decidable one.

There is also an efficiency argument, which is why the rule places the single build at the commit stage specifically. Recompiling at each stage “takes time, especially in large systems,” and the whole point of the pipeline is fast feedback. Building once and retrieving the stored artifact for later stages is both safer and faster.

Code Is Constant, Configuration Is Variable

The rule forces a design discipline that is valuable in its own right. If the same artifact must deploy to test, staging, and production, then everything that legitimately differs between those environments — database URLs, credentials, feature-flag defaults, log levels, replica counts — cannot be baked into the artifact. It must be supplied from outside, at deploy time. Humble and Farley: the principle “forces you to separate code, which remains the same between environments, and configuration, which differs between environments” (Humble & Farley).

flowchart TD
    ART[("Immutable artifact<br/>sha256:abc123<br/>the SAME everywhere")]
    CFGT["Test config<br/>db=test.internal<br/>log=DEBUG"]
    CFGS["Staging config<br/>db=stage.internal<br/>log=INFO"]
    CFGP["Prod config<br/>db=prod.internal<br/>log=WARN"]

    ART --> RT["Test runtime"]
    CFGT --> RT
    ART --> RS["Staging runtime"]
    CFGS --> RS
    ART --> RP["Prod runtime"]
    CFGP --> RP

What it shows and the insight to take: the artifact is the invariant input across all three environments; the configuration is the variant input, joined to the artifact only at deploy time in each environment. The running process is artifact + environment-specific config. This is why baking a production database URL into a container image at build time is a design error under this rule — it makes the image environment-specific and therefore un-promotable. Config is injected via environment variables, mounted config files, a config service, or (on Kubernetes) ConfigMap/Secret objects — never compiled in. The provisioning of those config sources is owned by Infrastructure as Code MOC; the pipeline’s job is simply to keep the artifact free of them.

Uncertain

Verify: whether secrets specifically should be injected by the same deploy-time-config mechanism as ordinary configuration, or handled by a dedicated secrets manager with short-lived credentials. Reason: Humble & Farley predate the modern secretless/OIDC pattern; the “config injected at deploy” principle covers non-secret config cleanly but the secret-handling best practice has since evolved. To resolve: for the current pipeline-secrets model see Secret Injection in Pipelines and OIDC and Secretless Pipeline Authentication — the principle (nothing environment-specific baked into the artifact) holds; the mechanism for secrets is more specialized. #uncertain

The Artifact Must Be Immutable and Uniquely Addressable

“Build once, promote” only works if the artifact you promote is provably the same artifact throughout. That requires two properties: immutability (once published, the artifact never changes) and unique addressability (you can name the exact artifact you are promoting). Content-addressed storage delivers both.

Container images are the canonical modern example. The OCI Distribution Spec addresses image content by digest — a cryptographic hash (typically SHA-256) computed over the image manifest (OCI Distribution Spec). A digest is self-verifying and immutable: sha256:abc123… refers to exactly one set of bytes, forever; if the bytes changed, the digest would change, so the same digest served by any registry, anywhere, is byte-identical. A tag (myapp:1.4.2 or myapp:latest), by contrast, is a mutable pointer — it can be re-pointed at different content later. The disciplined pipeline therefore promotes by digest, not by tag: it takes the digest that passed acceptance testing and deploys that digest to production, so there is zero ambiguity about which bytes are running. The versioning discipline around this — why re-tagging over an existing version is a supply-chain footgun — is the subject of Immutable Artifact Versioning; the registry mechanics of push/pull by digest are in Container Image Registries and OCI Image and Distribution Specifications.

sequenceDiagram
    participant CS as Commit Stage
    participant REG as Artifact Registry
    participant TEST as Test Deploy
    participant PROD as Prod Deploy
    CS->>CS: compile + package ONCE
    CS->>REG: push image → sha256:abc123
    REG->>TEST: pull sha256:abc123
    TEST->>TEST: run acceptance tests (PASS)
    Note over REG,PROD: promotion = reference the SAME digest
    REG->>PROD: pull sha256:abc123 (identical bytes)
    PROD->>PROD: run with prod config

What it shows and the insight to take: the artifact is pushed to the registry once, keyed by its digest. Both the test deploy and the prod deploy pull the same digest. Promotion is not a rebuild and not even a copy of new bytes — it is the act of referencing the already-stored, already-tested digest in the next environment. The registry is the single storage location Humble and Farley call for when they say to store binaries “where it is easy to retrieve them for later stages.”

Where This Sits Among the Pipeline Practices

“Build once” is one of six mutually reinforcing deployment-pipeline practices from Humble and Farley (Humble & Farley). Seeing them together shows why the rule is not an isolated preference but part of a coherent system:

PracticeWhat it saysHow it relates to “build once”
Only build your binaries onceCompile/package once, at the commit stage; reuse thereafterThe rule itself
Deploy the same way to every environmentUse one identical deployment process for dev, test, staging, prodSame process + same artifact ⇒ prod deploy is tested hundreds of times before it matters
Smoke-test your deploymentsAn automated check that the app started and its dependencies are reachableConfirms the promoted artifact actually runs in the new environment
Deploy into a copy of productionTest/staging environments mirror productionMakes “passed in staging” a trustworthy statement about prod
Each change propagates instantlyCommit triggers stage 1; each stage triggers the next on successThe promotion of the one artifact is automatic, not manual
If any part fails, stop the lineA failed stage halts the pipeline; the whole team owns the failureA broken promotion blocks everything downstream

What it shows and the insight to take: “build once” is the foundation the other practices rest on. “Deploy the same way to every environment” is only meaningful if it is the same thing being deployed each way; “deploy into a copy of production” only buys confidence if the artifact you tested there is the artifact you ship. Humble and Farley are explicit that deploying identically everywhere is what lets you trust the process: “only after you have tested the deployment process hundreds of times on many environments can you eliminate the deployment script as a source of error.” Build-once + deploy-identically together are what make the pipeline’s green lights honest.

Failure Modes and How to Diagnose Them

  • “Works in staging, breaks in prod” after a per-environment rebuild. The classic symptom of violating the rule. A dependency or compiler drifted between the staging build and the prod build. Diagnosis: compare the artifact digests of what ran in staging vs prod — if they differ, you rebuilt, and that difference is your prime suspect. Fix: promote the digest, don’t rebuild.
  • Baked-in config makes the artifact un-promotable. Someone hard-coded a staging database URL into the image, so the “same” image cannot go to prod. Diagnosis: the image only works in one environment. Fix: externalize the config; inject at deploy time.
  • Tag mutation causes “it changed under me.” Prod deploys myapp:1.4.2, which was silently re-pushed with different bytes. Diagnosis: the running digest doesn’t match the tested digest even though the tag matches. Fix: pin and promote by digest, enforce immutable tags (Immutable Artifact Versioning).
  • Latent non-determinism defeats “same source ⇒ same binary.” Even a single build can be non-reproducible if it embeds timestamps or unsorted inputs — but under build-once this matters less, because you promote the artifact, not the source. Reproducibility (Hermetic and Reproducible Builds) is a stronger, orthogonal guarantee; build-once is the minimum discipline that makes the pipeline trustworthy even without full reproducibility.

Relationship to Continuous Delivery

Build-once is what makes the deployment pipeline’s promise achievable. Fowler defines Continuous Delivery as building software so it “can be released to production at any time,” kept “in a perpetually deployable state” (Fowler, ContinuousDelivery). “Deployable at any time” is only credible if the thing you would deploy is the exact thing your pipeline already validated. Fowler’s deployment-pipeline definition — “a way to deal with this by breaking up your build into stages,” where “each stage provides increasing confidence, usually at the cost of extra time” (Fowler, DeploymentPipeline) — describes stages of increasing confidence in one artifact. If each stage tested a different artifact, the confidence would not accumulate; it would reset at every rebuild. Build-once is the invariant that lets confidence compound as the artifact advances. The strategy for how much traffic that promoted artifact then takes (canary, blue-green, flag ramp) is a separate concern owned by Site Reliability Engineering MOC; build-once simply guarantees that whatever strategy runs, it is running the artifact you tested.

See Also