I’ve lost count of how many times I’ve seen a production incident traced back to one root cause: someone deployed myapp:latest and it wasn’t the version they thought it was. Tagging seems trivial until it isn’t, and getting it right is one of the highest-leverage things you can do for a Docker-based delivery pipeline. In this guide I’ll cover exactly how Docker tags work under the hood, the tagging strategies I actually use in production, and the pitfalls that cause the most pain.
What a Docker Tag Actually Is
A Docker image is identified internally by a content-addressable SHA256 digest — that’s the real, immutable identity of an image. A tag is just a human-readable pointer, stored in the registry’s manifest index, that maps a string like myapp:1.4.2 to one specific digest at a point in time. Critically, tags are mutable. You can push a new image under an existing tag and the old mapping is simply overwritten — the previous image still exists in the registry (assuming nothing garbage-collects it), but the tag no longer points to it.
This mutability is the source of almost every tagging problem people run into, and it’s why the digest, not the tag, is the only thing that guarantees you’re running exactly the bits you think you are.
You can see this yourself:
docker pull nginx:1.27
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.27
Expected output:
nginx@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
That sha256:... string is the real identity. nginx:1.27 is just a label someone attached to it.
The Anatomy of a Tag Name
The full reference format for a tagged image is:
[registry-host[:port]/]namespace/repository[:tag]
For example:
registry.example.com:5000/my-team/myapp:2.3.1
If you omit the registry host, Docker assumes Docker Hub (docker.io). If you omit the tag entirely, Docker assumes :latest.
Basic Tagging Commands
Tag an image you just built:
docker build -t myapp:1.0.0 .
Add an additional tag to an already-built image:
docker tag myapp:1.0.0 myapp:latest
Tag for a specific registry so you can push it there:
docker tag myapp:1.0.0 registry.example.com/my-team/myapp:1.0.0
docker push registry.example.com/my-team/myapp:1.0.0
Verify what tags exist locally for an image:
docker images myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 1.0.0 3f9a1c2b4d5e 2 minutes ago 142MB
myapp latest 3f9a1c2b4d5e 2 minutes ago 142MB
Notice both tags share the same IMAGE ID — that confirms they point to the identical underlying image, just under two different names.
Why :latest Is Dangerous
:latest is not a special “always the newest” tag maintained automatically by Docker — it’s just the default tag applied when you don’t specify one. Nothing stops you (or a teammate, or a CI job) from pushing an old image under :latest by accident. The problems I see most often:
- Non-reproducible deployments. If your Kubernetes manifest says
image: myapp:latest, two nodes pulling at different times can end up running two different actual images, because the tag moved in between pulls. - No rollback story. If production breaks and you only ever push
:latest, you have no way to know what the previous “latest” even was unless you kept separate records. - Cache confusion. With
imagePullPolicy: IfNotPresentin Kubernetes, a node that already has an older:latestcached will keep using it silently, while a node that pulls fresh gets the new one.
My rule: :latest is fine for local development and quick experiments, but it should never be what your production deployment pipeline references.
Tagging Strategies That Actually Work
1. Semantic Versioning (SemVer)
The most common strategy for libraries and applications with a clear release cadence:
docker build -t myapp:1.4.2 .
docker tag myapp:1.4.2 myapp:1.4
docker tag myapp:1.4.2 myapp:1
This gives consumers a choice of how much stability versus freshness they want — pinning to 1.4.2 for exact reproducibility, 1.4 to get patch updates automatically, or 1 to get minor updates automatically. This is the same pattern official images like node, python, and postgres use on Docker Hub.
2. Git-SHA-Based Tags
For internal services deployed many times a day, I tag with the Git commit SHA so every image is traceable directly back to the exact source code that produced it:
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t myapp:${GIT_SHA} .
docker push registry.example.com/my-team/myapp:${GIT_SHA}
This is extremely useful for debugging — if a pod is misbehaving, kubectl describe pod shows you the exact image tag, and from that you know precisely which commit is running.
3. Timestamp-Based Tags
Useful for artifacts built on a schedule (nightly builds, data pipeline snapshots):
TAG=$(date +%Y%m%d%H%M%S)
docker build -t myapp:${TAG} .
4. Environment-Qualified Tags
Some teams encode environment into the tag itself, though I generally prefer keeping environment out of the image tag and instead controlling it via separate deployment manifests, since the same image artifact should ideally be promoted unchanged from staging to production:
docker tag myapp:1.4.2 myapp:1.4.2-staging
5. Combining Strategies (My Preferred Approach)
In most CI pipelines I set up, a single build produces multiple tags simultaneously:
GIT_SHA=$(git rev-parse --short HEAD)
VERSION=$(cat VERSION)
docker build -t myapp:${GIT_SHA} .
docker tag myapp:${GIT_SHA} myapp:${VERSION}
docker tag myapp:${GIT_SHA} myapp:latest
docker push myapp:${GIT_SHA}
docker push myapp:${VERSION}
docker push myapp:latest
This gives you an immutable, traceable tag (GIT_SHA) for audits and rollbacks, a human-readable version tag for release notes, and a convenience latest tag for local pulls — without ever relying on latest for anything that matters operationally.
Tagging in a Dockerfile-Driven CI Pipeline
Here’s a GitHub Actions workflow I use as a template for tag automation on release:
name: build-and-push
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set version from tag
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: Log in to registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.example.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and tag
run: |
docker build -t registry.example.com/my-team/myapp:${VERSION} .
docker tag registry.example.com/my-team/myapp:${VERSION} registry.example.com/my-team/myapp:latest
- name: Push
run: |
docker push registry.example.com/my-team/myapp:${VERSION}
docker push registry.example.com/my-team/myapp:latest
Expected result after a tagged push like v2.3.1:
registry.example.com/my-team/myapp:2.3.1
registry.example.com/my-team/myapp:latest
Using Docker Compose with Version Tags
services:
web:
image: registry.example.com/my-team/myapp:2.3.1
ports:
- "8080:80"
Pinning the exact tag here means docker compose pull && docker compose up -d behaves identically across every machine that runs it — no ambiguity about what “current” means.
Referencing Images by Digest for Maximum Reproducibility
For the strictest reproducibility guarantees (common in regulated environments or GitOps pipelines), you can skip tags entirely and reference the digest:
services:
web:
image: registry.example.com/my-team/myapp@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
This is the only reference format that is truly immutable — even if someone re-pushes a tag, the digest reference is unaffected because it points directly at content, not at a mutable pointer.
Kubernetes Tagging Best Practices
In Kubernetes manifests, always avoid :latest combined with imagePullPolicy: IfNotPresent (the default when a tag other than latest is used, but Kubernetes forces Always specifically when the tag is latest — a detail worth knowing):
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
template:
spec:
containers:
- name: myapp
image: registry.example.com/my-team/myapp:2.3.1
imagePullPolicy: IfNotPresent
Because the tag is immutable-by-convention (2.3.1 should never be overwritten once published), IfNotPresent is safe here and avoids unnecessary registry pulls on every pod restart.
Cleaning Up Old Tags
Registries fill up fast when every CI run pushes a new tag. Most registries (Docker Hub, GitHub Container Registry, Harbor, ECR) support retention policies. As an example, an AWS ECR lifecycle policy that keeps only the last 10 images per repository:
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
}
]
}
Troubleshooting Common Tagging Problems
- “manifest unknown” on pull — the tag was deleted or never pushed to that registry; double check
docker imageslocally versus what’s actually present remotely withdocker manifest inspect. - Pod running old code despite a new push — you likely pushed to
:latestwithimagePullPolicy: IfNotPresentsomewhere the pod was already cached; force a repull or switch to immutable, unique tags. - CI overwrote a production tag by accident — this is exactly why many teams enforce tag immutability at the registry level (ECR, GHCR, and Harbor all support “tag immutability” settings that reject a push to an existing tag).
Summary
Tags are just mutable pointers to immutable content — once you internalize that single fact, most tagging mistakes become obvious in hindsight. Use semantic versioning or Git-SHA tags for anything that matters, reserve latest for convenience and local development only, and reference images by digest wherever true immutability matters most. Combine multiple tags per build so you get both human readability and machine traceability, and enable registry-side tag immutability if your team has ever been burned by an accidental overwrite.
References
- Docker Documentation — Image Tags: https://docs.docker.com/engine/reference/commandline/tag/
- Docker Documentation — Content Trust and Digests: https://docs.docker.com/engine/security/trust/
- OCI Image Format Specification: https://github.com/opencontainers/image-spec
- Kubernetes Documentation — Image Pull Policy: https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy
- AWS ECR Lifecycle Policies: https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html