A few moons ago I built a simple tool called RedisAdministrator. I used to just push changes to master because it was future Carls problem and then build the docker image tagged latest.
This was a 💩 strategy as the changes should be versioned and cater for breaking changes. Current Carl fixed it, you can see v1.0.0 here ❤️
One thing I did change was moving over to the options pattern for config instead of injecting values as environmental variables. This broke a long standing way of starting the application but I noticed that consumers were still working. I did not understand why until I looked at the value being used. It was carlpaton/redis-administrator:latest@sha256:529a5813d33a2b069bac480a4eba9a0bdfabab327c81b8332959f31ee625ab4e
The SHA is a Docker Hub SHA, more accurately called a Docker Image Digest. It is not a Git commit SHA as I initially thought.
The bit I was missing is that latest and @sha256:... are not equivalent. latest is a mutable tag that can move to a newer image, while the digest points at one exact image manifest. That means a consumer pinned to @sha256:529a... can keep pulling that old image even though latest now points somewhere else.
Commands that prove it
Here are the Docker commands and outputs I ran while updating this post.
Pull the image by tag:
1 | docker pull carlpaton/redis-administrator:latest |
1 | Digest: sha256:056a83a595d606286c0b60748a18e86b3536c2afbbfedf4d0656265c5fe88849 |
Inspect the local image and show the repo digest:
1 | docker image inspect carlpaton/redis-administrator:latest --format '{{index .RepoDigests 0}}' |
1 | carlpaton/redis-administrator@sha256:056a83a595d606286c0b60748a18e86b3536c2afbbfedf4d0656265c5fe88849 |
This is why you can see a full image reference like:
1 | carlpaton/redis-administrator:latest@sha256:056a83a595d606286c0b60748a18e86b3536c2afbbfedf4d0656265c5fe88849 |
That is the current state of latest, but the older digest from my original note still exists in Docker Hub:
1 | docker pull carlpaton/redis-administrator@sha256:529a5813d33a2b069bac480a4eba9a0bdfabab327c81b8332959f31ee625ab4e |
1 | docker.io/carlpaton/redis-administrator@sha256:529a5813d33a2b069bac480a4eba9a0bdfabab327c81b8332959f31ee625ab4e: Pulling from carlpaton/redis-administrator |
That is the key point: latest has moved to 056a..., but the older 529a... manifest is still available and can still be pulled directly.
The exact combined reference from the post also still resolves today:
1 | docker pull carlpaton/redis-administrator:latest@sha256:529a5813d33a2b069bac480a4eba9a0bdfabab327c81b8332959f31ee625ab4e |
1 | docker.io/carlpaton/redis-administrator@sha256:529a5813d33a2b069bac480a4eba9a0bdfabab327c81b8332959f31ee625ab4e: Pulling from carlpaton/redis-administrator |
If you want to prove these are different images, inspect both digests:
1 | docker image inspect carlpaton/redis-administrator:latest --format '{{.Id}}|{{.Created}}|{{index .RepoDigests 0}}' |
1 | sha256:056a83a595d606286c0b60748a18e86b3536c2afbbfedf4d0656265c5fe88849|2025-07-03T09:56:13.587152976Z|carlpaton/redis-administrator@sha256:056a83a595d606286c0b60748a18e86b3536c2afbbfedf4d0656265c5fe88849 |
So the consumer kept working because they were effectively pinned to the 2019 image digest, not because latest was still pointing at that image. Today latest resolves to the 2025 image digest instead.
If you see a different digest for latest in future, that is expected. latest is mutable, while @sha256:... is immutable.