Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Tag and version Docker images

Learn how to tag and version your Docker images effectively. This lesson covers naming conventions, semantic versioning, and best practices for image management in real-world workflows.

Focus: tag and version your docker images

Sponsored

Have you ever pulled an image from Docker Hub and wondered whether latest points to a stable release or a night's reckless commit? Or spent an afternoon debugging a production crash only to discover you deployed an untagged my-app that got silently overwritten? Untagged Docker images are the silent killers of reproducibility. Without a deliberate naming and versioning strategy, you lose the ability to roll back, audit changes, or collaborate confidently across environments. This lesson hands you a battle-tested system for tagging and versioning your Docker images so you never confuse staging with production again.

The problem this lesson solves

Docker tags are not semantic—they're just arbitrary strings. docker build -t my-app produces a mutable reference that, the next time you run the same command, replaces the old image with the new one. If you push both to a registry, my-app:latest becomes a race condition: which build is currently "latest"? Without explicit versioning, you can't:

  • Audit which version ran in production last week.
  • Guarantee that your CI/CD pipeline promotes the exact same artifact across environments.
  • Roll back to a known-good image after a bad deployment.

This is the same problem that led the broader software ecosystem to semantic versioning (SemVer). Docker doesn't enforce it, but your workflow must.

Core concept / mental model

Think of a Docker image tag as a nickname for a specific image digest. The digest is an immutable SHA256 hash of the image layers—it never changes. A tag, on the other hand, is a mutable pointer you can move from one digest to another.

Image digest: sha256:a1b2c3d4...
  ├─ tag: my-app:1.0.0   (immutable pointer — you decide)
  ├─ tag: my-app:latest  (mutable pointer — moves with each push)
  └─ tag: my-app:v1      (alias — also mutable)

Semantic versioning (MAJOR.MINOR.PATCH) is the de facto standard for software releases. Docker adapts it naturally:

  • MAJOR — breaking changes to the interface or behavior.
  • MINOR — backward-compatible feature additions.
  • PATCH — bug fixes, security patches, non-functional changes.

A well-tagged image uses a name that includes the repository (your Docker Hub, ECR, or GCR URI), the service name, and the version. Example:

registry.example.com/payments-api:1.4.2

This three-part string encodes everything you need to identify, retrieve, and deploy a specific artifact.

How it works step by step

Building a versioned image is a five-step cadence that you'll repeat every release:

  1. Choose a version — following SemVer, decide the next MAJOR.MINOR.PATCH value based on your changes.
  2. Tag during build — use docker build -t <name>:<version> or the --tag flag multiple times.
  3. Optionally alias — add a short alias like v1 or stable for convenience, but never overwrite the SemVer tag.
  4. Push to registrydocker push <name>:<version> makes it available to other environments.
  5. Consume by tag — in your Docker Compose, Kubernetes manifests, or CI scripts, reference the exact version tag, not latest.

Here's a concrete workflow for a user-service:

# Step 1: you decide this release is 2.1.0
VERSION=2.1.0
REGISTRY=docker.io/myteam

# Step 2: build and tag
 docker build -t $REGISTRY/user-service:$VERSION \
              -t $REGISTRY/user-service:latest \
              -t $REGISTRY/user-service:v2 .

# Step 3: push all tags
 docker push $REGISTRY/user-service:$VERSION
 docker push $REGISTRY/user-service:latest
 docker push $REGISTRY/user-service:v2

Pro tip: Always tag with at least the full SemVer (2.1.0) and one shorter alias (v2, stable). This gives you granularity for precise rollbacks and convenience for everyday use.

Hands-on walkthrough

Let's apply this to a minimal Flask app. Create app.py and Dockerfile—you can adapt this to any language.

# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]
# app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, tagged world!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Now build and tag it with a version:

export VERSION=1.0.0
export REPO=yourdockerhub/myapp

docker build -t $REPO:$VERSION \
             -t $REPO:latest \
             -t $REPO:v1 .

docker push $REPO:$VERSION
docker push $REPO:latest
docker push $REPO:v1

Verify that all three tags point to the same digest:

docker images | grep $REPO
# REPOSITORY              TAG       IMAGE ID       CREATED          SIZE
# yourdockerhub/myapp     1.0.0     abc123def456   2 minutes ago    125MB
# yourdockerhub/myapp     latest    abc123def456   2 minutes ago    125MB
# yourdockerhub/myapp     v1        abc123def456   2 minutes ago    125MB

Now simulate a patch release. Change the response string in app.py to "Hello, v1.0.1!", increment the version, rebuild, and push:

export VERSION=1.0.1
docker build -t $REPO:$VERSION -t $REPO:latest .
docker push $REPO:$VERSION
docker push $REPO:latest

Notice that v1 still points to the old 1.0.0—your v1 alias is frozen to that major version. If a bug is found in 1.0.0, you can roll back by deploying $REPO:1.0.0.

Expected output: After pushing 1.0.1, running docker images shows three images: 1.0.0, 1.0.1, and latest points to the newest. The v1 tag remains on 1.0.0 unless you overwrote it.

Compare options / when to choose what

Tagging strategy Pros Cons Best for
Full SemVer (1.2.3) Maximum traceability, easy rollbacks Longer to type, requires frequent updates Production deployments, CI/CD pipelines
Major.minor (1.2) Readable, groups patches Misses patch specificity Release candidates, pre-production
Short alias (v1, stable) Convenient for scripts Ambiguous when multiple patches exist Internal tooling, demo environments
Latest only Simple Breaks reproducibility, no history Unacceptable for anything except local testing

When to choose what: - Production — always pin to full SemVer: my-app:2.3.1. - CI/CD integration — push latest for convenience but never deploy it to production. - Rolling updates — use v2 as a stable alias for the latest 2.x.x release.

Troubleshooting & edge cases

Common mistakes

  • Mutating an existing SemVer tag: Never do docker build -t my-app:1.0.0 . a second time. If the content changes, increment the version. The old tag is now orphaned and users may pull a different image than expected.
  • Forgetting to push all tags: docker push without a tag pushes only the :latest tag by default. Always specify the full tag name.
  • Using latest in production manifests: When Kubernetes or Docker Compose resolves my-app:latest, it may pull a newer version than intended, causing cascading failures.

Edge cases

  • Multiple registries: If your image lives in Docker Hub, ECR, and GCR, maintain separate tag lists. A common pattern is to tag with all registry URIs during build:
docker build -t docker.io/myteam/web:1.0.0 \
             -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/web:1.0.0 .
  • Git SHA tags: For ephemeral environments, use the short Git commit hash as an auxiliary tag:
export GIT_SHA=$(git rev-parse --short HEAD)
docker build -t my-app:1.0.0 -t my-app:sha-$GIT_SHA .
  • Rollback: Deploy the previous SemVer tag. Because it's immutable, you know exactly what you're getting. docker pull my-app:1.0.0 is safe.

What you learned & what's next

You now understand that tagging and versioning your Docker images isn't an afterthought—it's a discipline that protects your deployments from chaos. You can:

  • Explain the difference between a mutable tag and an immutable digest.
  • Apply semantic versioning to Docker images.
  • Build, tag, and push images with multiple version labels.
  • Choose the right tagging strategy for your environment.
  • Troubleshoot common tagging pitfalls.

With this foundation, you're ready to explore Docker Compose version pinning—where you'll lock service images to exact versions in your docker-compose.yml to create reproducible multi-container stacks.

Practice recap

  1. Modify the sample Flask app to return a different string. 2. Build and tag the new image as 1.0.1, preserving the previous 1.0.0. 3. Push both tags. 4. Verify with docker images that v1 still points to 1.0.0 and latest points to 1.0.1. 5. In a docker-compose.yml, pin the service image to 1.0.0 and run — confirm it uses the old version.

Common mistakes

  • Rebuilding and pushing the same SemVer tag (e.g., 1.0.0) with different content — this breaks reproducibility and makes old digests unreachable.
  • Forgetting to push all tags; docker push my-app only pushes :latest, leaving your versioned tag local.
  • Using :latest in production manifests — a newer push overwrites the tag, causing deployments to drift without explicit version control.
  • Mixing tag schemes across environments (e.g., v1.0.0 in staging, 1.0.0 in prod) — inconsistency leads to confusion during rollbacks.

Variations

  1. Use Git commit SHA as an auxiliary tag for precise traceability: docker build -t my-app:1.0.0 -t my-app:sha-a1b2c3d .
  2. Adopt CalVer (calendar versioning) like 2025.03.01 for projects where time-based releases make more sense than SemVer.
  3. Attach multiple registry tags in a single build command to support both public Docker Hub and private ECR/GCR repositories.

Real-world use cases

  • CI/CD pipeline stores every build as my-app:1.2.3 and pushes :latest only for the current commit — staging consumes :latest, production pins to 1.2.3.
  • Rollback to previous version in Kubernetes by changing the image tag in your Deployment from my-app:2.0.1 to my-app:1.9.8 — because 1.9.8 was never overwritten.
  • Global release management: multiple teams publish services as team-a/auth:3.1.0 and team-b/payments:2.0.4, enabling independent rollbacks without coordination.

Key takeaways

  • Docker tags are mutable pointers to immutable image digests — treat SemVer tags as immutable.
  • Every release must increment the version at least at the patch level.
  • Always push the full SemVer tag to your registry — never rely solely on :latest.
  • Use short aliases (e.g., v2, stable) for convenience but preserve the full SemVer for audits.
  • Tag with multiple registry URIs if your image lives in different registries.
  • Rollback is only safe when you have preserved the exact version tag from the previous deployment.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.