A Docker image tag is a mutable pointer to a specific image, while its digest is an immutable SHA256 hash that uniquely identifies that exact image content.

Let’s see this in action. Imagine you’re building a web application. You’ve got a Dockerfile that looks like this:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
COPY index.html /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

You build this image and tag it my-web-app:latest:

docker build -t my-web-app:latest .

Now, someone else on your team pulls my-web-app:latest. They’ll get an image tagged my-web-app:latest. But what if you pushed a new version of index.html and rebuilt my-web-app:latest? The latest tag would now point to a different image. Your teammate, expecting the old version, would suddenly be running the new one, potentially breaking their environment. This is the fundamental problem of mutable tags.

The solution lies in the image digest. When you build an image, Docker generates a unique SHA256 hash for the resulting image content. You can see this digest using docker images --digests:

REPOSITORY      TAG       DIGEST                                                                    IMAGE ID       CREATED         SIZE
my-web-app      latest    sha256:a1b2c3d4e5f67890...                                              abcdef123456   2 minutes ago   150MB
<none>          <none>    sha256:fedcba0987654321...                                              ghijkl789012   3 minutes ago   149MB

Notice that my-web-app:latest has a digest. If you were to rebuild the image after changing index.html, the digest associated with my-web-app:latest would change, and a new digest would appear for the previous image (now <none>). To ensure reproducibility, you should pin your images using their digests.

Instead of referencing my-web-app:latest in your Docker Compose file or Kubernetes deployment, you should use the specific digest:

# docker-compose.yml
services:
  web:
    image: my-web-app@sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
    ports:
      - "80:80"

When you pull an image by its digest, Docker guarantees you get that exact set of layers and configuration, regardless of what tags are currently pointing to it. This eliminates the "it worked on my machine" problem caused by unexpected image updates.

The process usually involves building your image, pushing it to a registry with a mutable tag (like latest or a version number), and then inspecting the registry or your local Docker daemon to get the digest of the pushed image. Then, you update your deployment configurations to use that digest.

Here’s how you’d get the digest of an image after it’s been pushed to a registry (e.g., Docker Hub):


docker image inspect my-web-app:latest --format '{{index .RepoDigests 0}}'

# Output will be something like: my-registry/my-web-app@sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890

You then take this full repository@digest string and use it in your deployment configuration. This ensures that no matter how many times my-web-app:latest is pushed or updated, your deployment will always pull the specific, known-good image identified by that digest.

A common misconception is that using a version tag like my-web-app:1.2.3 is sufficient for reproducibility. While better than latest, it’s still mutable. The maintainer of the 1.2.3 tag could potentially push a new image with the same tag. Only the digest provides an immutable guarantee.

The image@digest syntax is the standard way to reference images immutably across different container orchestration systems and Docker itself. It’s the bedrock of reproducible, reliable deployments.

The next step in ensuring build reproducibility is managing the base images themselves, ensuring their digests are also pinned.

Want structured learning?

Take the full Docker course →