The main question you have to ask yourself when you want to version a docker image is whether you version the image or version the contents.
I don't know if there's a right answer to this, but what I do know is that over the last 3-4 years of using Docker the way I version images has definitely evolved.
latest as the tag for everything is pretty much where everyone starts
with docker. It's the default tag that docker will pull if you don't specify a
This gets old very quickly. Because
latest can now refer to several different
versions of the same image or software you have no real way of knowing what
version is actually running without some additional inspection.
Quite often you will have a failing build or application because the
the developer is using or deploying isn't actually the latest
docker build -t you/your-image:latest .
Versioning the Contents
Versioning the contents of an image seems like the next logical step. This is the way that official images are versioned on the docker hub.
If you've used images from the hub for any period of time, you are probably aware of the main issue with this: tags are not immutable.
This means you can pull an image like
node:6 and actually end up with a
different image to someone else who has an image tagged
node:6. You can become
a bit more specific and pull an image with
node:6.12.3 for example, but
there's still no guarantee that it's the same
node:6.12.3 that your teammates
Versioning the image
Versioning the image seems like the simplest solution to this problem. There are a few ways you can do this, and we've tried them all, including:
- image ids
- git commit hashes
This works but has one major drawback: Semantics. You now have no simple way of knowing what is included in an image, and no simple way of knowing what the tag should be - you have to go and look it up.
Further to this (with the exception of the timestamp), it's not very scripting friendly as you can't logically sort a list of tags. We very quickly dismissed image ids as you can pull and run images by id whether you tag them with it or not.
docker pull [email protected]:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
Base Images vs Application Images
Where we've landed is actually: do both. We have two types of images, Base Images, and Application Images. Base Images are used as the FROM for building Application Images, and Application Images are base images with your code added.
For us, all docker tags are immutable and we enforce that in our internal docker registry. Once an image tag is created it cannot be overwritten. This way, if you pull an image using a tag, you can be guaranteed it is the same image for everyone, everywhere.
For Application Images we version the image and the contents as a whole using SemVer and any change to the Dockerfile or the application inside the image requires a version bump.
This is fairly simple because we control the application. The version number here only means something to us, and our asset or artifact is the docker image itself. The code inside isn't released independently of the image, so they can be versioned together.
That looks like this:
Nice and simple.
Application Images are built using, and from Base Images. For Base Images we version everything. It leads to "busy" tags, but they are extremely clear.
Let's take our java images as an example. We have 3 types of image:
- A run image
- A build image
- A team or project image
The simplest of all of our images is the run image. This includes only the runtime for the language, and very little else. We version that like so:
Here we know it's the java image, that it's the runtime image, rather than the
build image, the included java version is 8u121, and it's version 1.0.0 of the
Dockerfile that built it.
If we were to change the Dockerfile without changing the version of java
contained, this would then be bumped to
java-run:8u121-1.0.1 depending on if it's a feature or a fix.
We then have a build image, and that looks like this:
Again, we know it's the java image, that it should be used for building and
therefore shouldn't be used in the FROM line of a production image, and that it
contains version 8u121 or the JRE (and the JDK), as well as version 3.3.9 of
maven, and it is version 1.0.0 of the
Finally, we use a team or project image that extends from the run image and adds any significant dependencies or tools required across multiple applications for a particular team or project. That looks like this:
This tells me again, it's java, meant to be used for the foo project, it contains java version 8u121, is built from version 1.0.0 of the parent java-run image, and it is version 2.0.0 of the foo-project base image Dockerfile.
So far this versioning pattern has scaled well across multiple languages, and to all of the teams and nearly 300 developers that we look after. In reality, we include a few extra things in our run images, but the rule holds fast: If we care about the version of the included tool, it goes in the tag.
For our java run image, we include the New Relic jar, for example, as all of our java projects require it. The New Relic jar is versioned independently and the version number is significant to us because we have to update it periodically to keep up with certain licensing conditions from New Relic, so it goes in the tag like so (where new relic is version 3.36.0):
This allows us to see that a service is using the expected version of certain significant dependencies at a glance in things like pull requests and so on. We also add labels in our docker images that allow us to run more intelligent tooling and static analysis against these versions, but so far, immutable, verbose, and semantic tagging is the best way we've found of versioning our images.