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
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 |
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
- Deploying `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.
- Treating artifacts as mutable. Never patch a built artifact in place. Build a new version. Immutability is what makes rollback and debugging possible.
- Rebuilding per environment. Build once, push, promote the same tag. A
docker buildin your prod deploy means prod isn't what you tested. - No traceability from artifact to commit. Tag with the commit SHA so you can always trace a running container back to its exact source.
- Hardcoding 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. - Never 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.
- `latest` is 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.
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.