Artifacts & Registries
The pipeline had a package stage that produced “the artifact.” This page
is about what that artifact is, why it must be immutable, and the single most important rule in all of
CD: build once, promote the same artifact everywhere. Get this right and a whole category of
“works in staging, breaks in prod” disasters simply cannot happen.
What an artifact is
Section titled “What an artifact is”An artifact is the concrete, deployable output of your build — the thing you actually ship. Depending
on your stack it might be a container image, a .jar, a compiled binary,
a tarball, or a Python wheel. For most modern services it’s a container image, and that’s what we’ll use
for examples, but the principles are universal.
The defining property of a good artifact is that it is immutable: once built, it never changes. You
don’t patch it, you don’t rebuild it “with a small fix” under the same name — if the inputs change, you
build a new artifact with a new identity. Immutability is what lets the artifact be a stable unit of
truth: when you say “version a1b2c3 is in production,” that string refers to one exact pile of bytes,
forever.
A registry is where artifacts live
Section titled “A registry is where artifacts live”A registry is the storage-and-distribution service for artifacts. For container images that’s a container registry (Docker Hub, GitHub Container Registry, ECR, Harbor); for language packages it’s npm, PyPI, Maven Central, or a private equivalent. The pipeline pushes a built artifact to the registry; every environment later pulls the very same one. The registry is the shared source of “the bytes,” sitting between build and deploy.
BUILD ONCE PROMOTE THE SAME ARTIFACT ────────── ───────────────────────── build ──► push ──► [REGISTRY] ──► pull ──► staging ✅ tested these bytes │ ──► pull ──► canary ✅ same bytes │ ──► pull ──► prod ✅ same bytes one image, one digestTags lie; digests don’t
Section titled “Tags lie; digests don’t”Here is the subtlety that bites people. Artifacts are usually referenced two ways:
- A tag is a human-friendly, mutable label:
app:latest,app:1.4.0. The catch is that a tag is just a pointer — you can re-pointapp:1.4.0at a different image tomorrow.latestis the worst offender: it means “whatever was pushed most recently,” which is to say it means nothing reproducible. - A digest is a content hash, e.g.
app@sha256:9f86d0..., computed from the artifact’s actual bytes. It is immutable by construction: change one byte and the digest changes. A digest can only ever refer to one exact artifact, and the same artifact always has the same digest.
Tag (:1.4.0, :latest) | Digest (@sha256:…) | |
|---|---|---|
| Stability | Mutable — can be re-pointed | Immutable — fixed forever |
| Refers to | ”Whatever this label points at now” | One exact set of bytes |
| Safe for promotion? | No — staging and prod could diverge | Yes — guaranteed identical |
A practical pattern: tag by the immutable commit hash at build time, then resolve and pin the digest for deployment.
# In the pipeline: tag with the commit SHA (never reused) and push.docker build -t ghcr.io/acme/app:${GITHUB_SHA} .docker push ghcr.io/acme/app:${GITHUB_SHA}
# Capture the digest the registry assigned — this is the real identity.docker buildx imagetools inspect ghcr.io/acme/app:${GITHUB_SHA} \ --format '{{json .Manifest.Digest}}'# -> "sha256:9f86d0818827...": deploy THIS to every environmentPromotion: move the artifact, not the source
Section titled “Promotion: move the artifact, not the source”“Promotion” is moving a single artifact through your environments — dev → staging → production — without rebuilding it. This is the rule the overview flagged, and it deserves its own moment, because the naive alternative is so tempting and so wrong: building separately for each environment (“build for staging, then build for prod”). Two builds from the “same” source are not guaranteed identical — a dependency could publish a new patch between builds, a base image could shift, a build tool could update. So the bytes you tested in staging may differ from the bytes you ship to prod, and your testing guaranteed nothing about what’s actually running.
Promotion fixes this by making the artifact the unit that moves. You build once, test that artifact, and promote the identical digest forward. What changes per environment is configuration — database URLs, secrets, replica counts — injected at deploy time via config and secrets, never baked into the artifact. The artifact is environment-agnostic; only the surrounding config differs.
❌ rebuild per env ✅ promote one artifact src ─► build ─► staging src ─► build ─► [digest] ─► staging src ─► build ─► prod (different!) └──────► prod (identical) bytes may differ same bytes + per-env configThis is the book’s thread sharpened to a point. The manual, error-prone step being removed is rebuilding and hoping the result matches — and the implicit, unstated assumption that “same source means same binary,” which simply isn’t true. Immutable, digest-addressed artifacts replace that hope with a guarantee: the exact bytes that passed every gate in the pipeline are the exact bytes serving production traffic. It also makes rollback trivial — the previous good artifact still exists in the registry, unchanged, ready to redeploy by its digest. You’re never reconstructing a past state; you’re re-pointing at one that was never destroyed.
The architect’s lens
Section titled “The architect’s lens”Step back from tags and digests and answer the five questions:
- Why does it exist? Because “same source” does not mean “same bytes” — a dependency patch or a shifted base image between two builds breaks that assumption, so we need one immutable artifact addressed by its content.
- What problem does it solve? “Works in staging, breaks in prod”: build once, push to a registry, and
promote the identical digest (
@sha256:…) through every environment, so what you tested is provably what you ship. - What are the trade-offs? You must keep config out of the artifact (inject DB URLs and secrets at deploy time), and you take a hard dependency on the registry as a shared, must-stay-available system.
- When should I avoid it? Rarely for the principle — but pinning to a digest costs you the convenience of
a floating tag, so for throwaway local dev a moving
:latestis fine. - What breaks if I remove it? Deploy
:latestand two environments can silently run different images; rebuild per environment and your staging tests guarantee nothing — and rollback stops being “redeploy the previous digest, which still exists unchanged.”
Check your understanding
Section titled “Check your understanding”- What does it mean for an artifact to be immutable, and why is immutability what lets “version X is in prod” be a meaningful statement?
- Explain the difference between a tag and a digest. Why is
:latestdangerous in production? - Why can building separately for staging and for production produce different bytes even from the same git commit? What does that do to the value of your staging tests?
- In the promote-one-artifact model, what does legitimately differ between environments, and how is it supplied?
- Using the book’s thread, explain how digest-addressed promotion turns “it worked in staging” from a hope into a guarantee — and why it makes rollback easy.
Show answers
- Immutable means once built it never changes — you never patch or rebuild it under the same name. That’s
what makes “version
a1b2c3is in prod” meaningful: the string refers to one exact pile of bytes forever, so the statement is a fact about a specific artifact rather than a moving target. - A tag is a mutable, human-friendly pointer (
:1.4.0,:latest) that can be re-pointed at a different image later; a digest (@sha256:…) is a content hash, immutable by construction.:latestis dangerous because two environments both saying:latestcan run different images, and a redeploy can silently change what’s running with no code change. - Two builds from the “same” commit aren’t guaranteed identical — a dependency can publish a patch, a base image can shift, a build tool can update between them. So the staging bytes may differ from the prod bytes, which means your staging tests guaranteed nothing about what’s actually running in production.
- Only configuration legitimately differs — database URLs, secrets, replica counts — and it’s injected at deploy time via config and secrets, never baked into the artifact. The artifact itself is environment-agnostic.
- The exact bytes that passed every gate are the exact bytes serving prod, so “it worked in staging” becomes a guarantee rather than a hope — it removes the “rebuild and hope it matches” step. Rollback is easy because the previous good artifact still exists in the registry, unchanged; you re-point at it by digest instead of reconstructing a past state.