In our consultancy work, we often see companies tagging production images in an ad-hoc manner. Taking a look at their registry, we find a list of images like:
acmecorp/foo:latest
acmecorp/foo:v1.0.0-beta
acmecorp/foo:v0.3.0
acmecorp/foo:v0.2.1
acmecorp/foo:v0.2.0
acmecorp/foo:v0.1.0
and so on.
There is nothing wrong with using semantic versioning for your software, but using it as the only strategy for tagging your images often results in a manual, error prone process (how do you teach your CI/CD pipeline when to upgrade your versions?)
I'm going to explain you an easy, yet robust, method for tagging your images. Spoiler alerts: use the commit hash as the image tag.
Suppose the HEAD of our Git repository has the hash ff613f07328fa6cb7b87ddf9bf575fa01b0d8e43
. We can manually build an image with this hash like so:
docker build -t acmecorp/foo:ff613f07328fa6cb7b87ddf9bf575fa01b0d8e43 .
(out) Sending build context to Docker daemon 113.7kB
(out) Step 1/10 : FROM golang:1.9-alpine as builder
(out) ---> ed119d8f7db5
(out) Step 2/10 : WORKDIR /foo/bar
(out) ---> Using cache
(out) ---> 8633ad87b8a1
(out) Step 3/10 : COPY ./src .
(out) ---> Using cache
(out) ---> 4a5c2a3dec01
(out) Step 4/10 : RUN make build
(out) ---> Using cache
(out) ---> fb127fe8085e
(out) Step 5/10 : FROM alpine:3.6
(out) ---> a41a7446062d
(out) Step 6/10 : EXPOSE 80
(out) ---> Using cache
(out) ---> 34dfb86e2e2c
(out) Step 7/10 : RUN apk --no-cache add ca-certificates
(out) ---> Using cache
(out) ---> 56158bf4d1c9
(out) Step 8/10 : WORKDIR /foo/bar
(out) ---> Using cache
(out) ---> 877f6977599d
(out) Step 9/10 : COPY --from=0 /foo/bar/app .
(out) ---> Using cache
(out) ---> dae2793fa127
(out) Step 10/10 : CMD ./app
(out) ---> Using cache
(out) ---> 91a3a5da7d8b
(out) Successfully built 91a3a5da7d8b
(out) Successfully tagged acmecorp/foo:ff613f07328fa6cb7b87ddf9bf575fa01b0d8e43
If we know that this is the latest image tag (but beware of the confusion caused by latest!), we can also immediately tag it as such:
docker tag acmecorp/foo:ff613f07328fa6cb7b87ddf9bf575fa01b0d8e43 acmecorp/foo:latest
Once all that's done you can push your image to the registry:
docker push acmecorp/foo
(out) The push refers to a repository [docker.io/acmecorp/foo]
(out) ...
(out) latest: digest: sha256:57b73ae1110ffab17cce2824f2416dc5e96122035b083f282f8a6b009905adee size: 949
(out) latest: digest: sha256:57b73ae1110ffab17cce2824f2416dc5e96122035b083f282f8a6b009905adee size: 949
That works, but we don't really want to manually cut and paste the git hash each time, so let's see how we can fix that next.
Git Magic
To get the latest commit of your repository:
git log -1
(out) commit 18101e645ee3d9b1de302164bb31f907a8282349 (HEAD -> master, origin/master, origin/HEAD)
(out) Author: John Doe <john@doe.com>
(out) Date: Mon Sep 25 16:29:48 2017 +0200
(out)
(out) Render magic in front page
That's great information but quite verbose. Trim it down to just get the hash from the last commit:
git log -1 --pretty=%H
(out) 18101e645ee3d9b1de302164bb31f907a8282349
You can even get a shorter version of the hash but I prefer the other one:
git log -1 --pretty=%h
(out) 18101e6
Automation
I'm a big fan of Makefiles. I've been using them for all of my Go projects but also for anything that has Docker related tasks, like building containers, for example :)
A normal Makefile
for building projects with Docker would look more or less like this:
NAME := acmecorp/foo
TAG := $$(git log -1 --pretty=%!H(MISSING))
IMG := ${NAME}:${TAG}
LATEST := ${NAME}:latest
build:
@docker build -t ${IMG} .
@docker tag ${IMG} ${LATEST}
push:
@docker push ${NAME}
login:
@docker log -u ${DOCKER_USER} -p ${DOCKER_PASS}
Now it's just a matter of make build push
for you to generate a new image with automated tagging.
If you have a CICD pipeline in place for your project, it gets even easier. The only gotcha is that you need to be logged in with your registry before attempting to push the image there. That's what the login
task is for. Invoke it before invoking the push
task and you're good to go. Don't forget to add the DOCKER_USER
and the DOCKER_PASS
environment variables to your pipeline, otherwise the login task will not work.
Also do not call all the tasks in the same line. Best if you break it down into different steps.
Coming back to Semantic Versioning
As I said earlier in this post, there is nothing wrong with semversioning your software. If you offer an image that is consumed by many users you probably want to start tagging your stable releases that match with major milestones in your project's road map.
These are points in time that you want to look back easily, then having a something like v1.2.3
is something that not only you can benefit from but also your users. But then again, this a manual process that can't (?) be automated so it needs to be used in conjuction with something like what I have proposed in here.
Conclusions
- Whenever possible you should automate the generation of your container images. If you can think of a scenario where this is not a good idea, please leave a comment below.
- Automatically tagging your container images should not be difficult. Think of a streamlined process to achieve this. So far we have used this approach with our customers and they are all quite happy with it. Let me know in the comments if you have a different approach set in place that is also working for you
- One of the main benefits from tagging your container images with the corresponding commit hash is that it's easy to trace back who a specific point in time, know how the application looked and behaved like for that specifc point in history and most importantly, blame the people that broke the application ;)
- There is nothing wrong with semantic versioning. Use that for your image tags together with this other dynamic way of tagging
If you'd like to learn more about Docker, join our workshop: