On this page
- Your pipeline builds something. Where does it go?
- What is a build artifact?
- Immutability: the property that makes it trustworthy
- Versioning and tags, and why latest is a trap
- What a registry is and how it works
- Build, tag, and push for real
- Build once, promote many
- Common mistakes that cost hours
- Takeaways
- Where to go next
TL;DR
Learn what your pipeline actually produces and where it lives: immutable build artifacts, meaningful version tags (and why latest is a trap), how registries work, and the build-once-promote-many pattern.
Your pipeline builds something. Where does it go?
You've got a pipeline that builds and tests your code. But "builds" produces a *thing*, and that thing has to live somewhere between being built and being deployed. That thing is a build artifact, and the place it lives is a registry. They're quietly central to how every team ships software, and they're the part beginners tend to hand-wave past.
Get this right and your deploys become trustworthy and repeatable. Get it wrong, especially the versioning, and you end up unable to answer the most basic question during an incident: "what is actually running in production right now?" This article makes sure you can always answer it.
Who this is for
Junior engineers who've built a CI pipeline but treat the output as a black box. We focus on container images (the most common artifact today) but the principles, immutability, versioning, promote-don't-rebuild, apply to any artifact: a JAR, a zip, an npm package.
What is a build artifact?
A build artifact is the packaged, ready-to-run output of your build, a single, self-contained unit that can be stored, versioned, and deployed without rebuilding from source.
Your source code isn't directly runnable in production, it has to be compiled, bundled, and packaged first. The result of that packaging is the artifact. For a containerized app it's a Docker image; for a Java app a .jar; for a serverless function a .zip. Whatever the format, the idea is the same: one frozen, deployable bundle.
Immutability: the property that makes it trustworthy
The single most important property of a good artifact is immutability, once built, it never changes. You don't patch it, edit it, or rebuild it in place. If you need a change, you build a *new* artifact with a *new* version. The old one stays exactly as it was, forever.
Why does this matter so much? Because immutability is what lets you reason about your system. If version v1.4.2 is running in production and you can pull that exact same v1.4.2 onto your laptop, you can debug precisely what users are hitting. If artifacts were mutable, if "the build" could quietly change underneath you, that guarantee evaporates, and you're back to "works on my machine." Immutability is also what makes rollback trivial: just redeploy the previous artifact, byte-for-byte.
Pro tip
Immutability is the same reason "build once, deploy many" works. The artifact you tested in staging is *provably* the artifact you ship to prod, because it literally cannot have changed in between. Mutable artifacts break that proof.
Versioning and tags, and why latest is a trap
An artifact without a meaningful version is nearly useless, you can't tell which is which. Every artifact gets a tag (a label) that identifies it. The quality of your tagging strategy directly determines whether you can answer "what's running in prod?"
Docker's default tag, latest, is the trap almost everyone falls into. latest doesn't mean "the newest", it's just a label that *moves* to whatever was pushed most recently. It points at different images over time, which destroys the one thing tags are for: knowing exactly what you have.
latest | Immutable tag (SHA / semver) | |
|---|---|---|
| Points to | Whatever was pushed last | One specific, fixed image |
| Reproducible deploy? | No, meaning drifts | Yes, always the same image |
| Can you roll back to it? | No, it already moved | Yes, it never moves |
| "What's in prod?" | Unanswerable | The exact tag, every time |
latest fails at the one job a tag has.Use tags that *pin*. Two strategies, often combined: the Git commit SHA (e.g. app:9f2a1c7) ties an image to the exact code that built it, perfect for traceability, and semantic versioning (e.g. app:1.4.2) communicates intent to humans. A common pattern is to push both: the SHA for machines, the semver for releases.
The `latest` 2am story
An incident hits. You check production: it's running myapp:latest. Which commit is that? Nobody knows, latest has been re-pushed a dozen times since. You can't reproduce it, can't roll back cleanly, and can't even be sure two servers are running the same image. This is why teams ban latest in production deploys.
What a registry is and how it works
A registry is the storage service where artifacts live, a versioned warehouse your CI pushes to and your deploy targets pull from. For container images, common registries are Docker Hub, GitHub Container Registry (GHCR), AWS ECR, Google Artifact Registry, and Azure ACR. They all speak the same protocol; the workflow is identical.
The flow is a simple loop, and it's the connective tissue between your pipeline and your running app:
- 1
CI builds the artifact
Your pipeline builds the image and tags it with the commit SHA (and maybe a version).
- 2
CI pushes to the registry
The tagged image is uploaded to the registry, now it's stored, versioned, and shareable.
- 3
Deploy pulls from the registry
Staging pulls that exact tag and runs it. After verification, prod pulls the same tag.
- 4
Rollback re-pulls an old tag
Need to revert? Re-deploy a previous tag from the registry. It's still there, unchanged.
Pro tip
Registries are also a security boundary. Use a private registry for anything proprietary, scan images for vulnerabilities on push (most registries do this), and lock down who can push. A poisoned image in a registry deploys straight to production.
Build, tag, and push for real
Here's the whole loop in actual commands. This is what your CI runs under the hood; running it by hand once makes it click.
# Use the Git commit SHA as an immutable tag
TAG=$(git rev-parse --short HEAD) # e.g. 9f2a1c7
REGISTRY=ghcr.io/your-org
IMAGE=$REGISTRY/myapp
# 1. Build, tagging with the immutable SHA
docker build -t $IMAGE:$TAG .
# 2. Also tag it as a human-readable version (optional)
docker tag $IMAGE:$TAG $IMAGE:1.4.2
# 3. Log in to the registry (token from a secret, never hardcoded)
echo "$REGISTRY_TOKEN" | docker login ghcr.io -u your-user --password-stdin
# 4. Push BOTH tags, they point at the same image bytes
docker push $IMAGE:$TAG
docker push $IMAGE:1.4.2Note that docker tag doesn't copy anything, it just adds a second name pointing at the same underlying image, so pushing both tags is cheap. Later, your deploy step pulls the specific tag and runs it:
# Pull and run the EXACT immutable tag, never `latest`
docker pull ghcr.io/your-org/myapp:9f2a1c7
docker run -d -p 3000:3000 ghcr.io/your-org/myapp:9f2a1c7
# Confirm exactly what's running (answers "what's in prod?")
docker ps --format 'table {{.Image}}\t{{.Status}}'Because the tag is the commit SHA, that last command doesn't just tell you the image is running, it tells you the *exact line of source code* in production. That traceability, from a running container all the way back to a Git commit, is the entire point.
Build once, promote many
This is the pattern everything in this article serves. You build the artifact once, push it to the registry, and then *promote that same artifact* through your environments, you never rebuild for staging and rebuild again for prod.
It works precisely *because* artifacts are immutable and registries store them: staging pulls myapp:9f2a1c7, you verify it, and then prod pulls the byte-identical myapp:9f2a1c7. The thing you tested is, provably, the thing you shipped. Rebuilding per environment throws that guarantee away, a dependency could shift between builds and you'd ship something subtly different from what you tested. We cover this from the pipeline angle in CI/CD Fundamentals.
Watch out
If your deploy step contains a docker build for production, you're rebuilding, not promoting, and you've lost the guarantee that prod matches what you tested. Production deploys should only ever pull an existing, tested tag from the registry.
Common mistakes that cost hours
- 1Deploying `latest` to production. It's a moving label, not a version. You can't reproduce it, roll back to it, or even know what it is. Ban it from prod deploys.
- 2Treating artifacts as mutable. Never patch a built artifact in place. Build a new version. Immutability is what makes rollback and debugging possible.
- 3Rebuilding per environment. Build once, push, promote the same tag. A
docker buildin your prod deploy means prod isn't what you tested. - 4No traceability from artifact to commit. Tag with the commit SHA so you can always trace a running container back to its exact source.
- 5Hardcoding registry credentials. Registry tokens are secrets, inject them from your CI's secret store and pipe to
--password-stdin, never put them in a script or Git. - 6Never cleaning up old tags. Registries fill up and cost money. Set a retention policy, but keep enough history that you can always roll back.
Takeaways
The whole article in seven lines
- An artifact is the packaged, ready-to-run output of your build (often a container image).
- Immutability is the key property: once built, it never changes, you build a new version instead.
- Tag with something that pins: the Git commit SHA and/or a semantic version.
latestis a trap, it moves over time, so it can't reproduce, roll back, or identify a build.- A registry stores artifacts: CI pushes, deploys pull, rollback re-pulls an old tag.
- Tag with the commit SHA for full traceability from a running container back to source.
- Build once, push, and promote the same tag through every environment, never rebuild for prod.
Where to go next
You know what artifacts are and where they live. Next, make sure the images you push are small and secure, see how this fits the wider pipeline, and practice the commands hands-on.
- Dockerfile Best Practices, the artifacts you push should be small, fast, and secure.
- CI/CD Fundamentals, where the build, push, and promote steps sit in the bigger flow.
- Practice in the Docker lab, build, tag, and push images in an in-browser terminal.
Check your understanding
1. Why is immutability described as the most important property of a good build artifact?
2. Why does the article call the latest tag a trap?
Frequently asked questions
What exactly is a build artifact?
A build artifact is the packaged, ready-to-run output of your build: a single, self-contained unit that can be stored, versioned, and deployed without rebuilding from source. For a containerized app it is a Docker image, for a Java app a .jar, and for a serverless function a .zip.
Why is immutability so important for artifacts?
Immutability means an artifact never changes once built, which lets you reason about your system: if v1.4.2 is in production, you can pull that exact same v1.4.2 onto your laptop and debug precisely what users hit. It also makes rollback trivial, since you just redeploy the previous artifact byte-for-byte.
Why is the latest tag a trap?
The latest tag is mutable and ambiguous, so it can point to different artifacts over time and you lose the ability to say exactly what is running. Use meaningful, immutable version tags so you can always answer 'what is actually running in production right now?'
What is build once, promote many?
It means you build a single immutable artifact, then promote that same artifact through staging and into production rather than rebuilding at each stage. Because the artifact literally cannot change in between, what you tested is provably what you ship.
Was this article helpful?
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.