GitLab’s continuous integration and deployment are great. If you have special runners, you can even build and deploy docker images of your software in the CI. This new possibility immediately leads to the following questions.
- When should you build a docker image?
- How should it you tag the docker image?
The answers depend on your project. However, I’d like to discuss a few general strategies which are in line with usual git release practices.
Overwrite latest
The most straightforward strategy is to build the images, tag them as latest
and push them to the registry. The following snippet of a GitLab CI job pushes
the image to the registry of the repository.
docker build -t ${CI_REGISTRY_IMAGE}:latest .
docker push ${CI_REGISTRY_IMAGE}:latest
At first, this might look like a good idea. The latest tag corresponds to the
most recent commit. However, when you rerun a CI job (because you need to
recreate some expired artifact or an old docker image), you will overwrite the
latest
image by an old version. The situation becomes even more difficult if
you consider feature branches. Whenever someone pushes to a feature branch, you
will overwrite the latest tag. If there are multiple active feature branches,
it is difficult to understand which features are included in the current latest
image. The set of features included in latest
will change regularly.
Using GitLab’s environments improves the situation slightly. This way, GitLab
tracks the latest deployment to the latest
environment. However, it doesn’t
solve the issue; it merely helps to keep track of the problem.
Use Git commit hash
Another approach, which is frequently recommended in online articles, is to use the Git hash value. The relevant code for a GitLab CI job looks as follows.
docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} .
docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
Since Git commit hashes are unique, we can be assured that images will not be overwritten. There is no problem in working on several feature branches at the same time.
This solution is not optimal. When should we build the images? For every commit?
This would quickly lead to a cluttered registry. What about the latest
tag?
With this strategy, we have to deal with latest
manually or pass cryptic hash
values to the target audience of the image.
Use tags
Docker uses tags to tag images, GitLab uses tags to tag commit. Sounds like a
perfect match.
Consider the following example .gitlab-ci.yml
configuration. It builds on top of the
previous solution. Images are tagged with the commit hash or with the Git tag if
present.
stages:
- build
# Template for docker-in-docker jobs, requires special runner with 'dind' tag
.dind: &dind_template
image: docker:git
stage: build
tags:
- dind
services:
- name: docker:dind
alias: docker-in-docker
variables:
DOCKER_HOST: tcp://docker-in-docker:2375/
# For non-tagged commits, manually build the image with the commit hash
build:manual:
<<: *dind_template
when: manual
# Use ${CI_COMMIT_SHORT_SHA} if you want to use shortend commit hashs
script:
- echo "${CI_REGISTRY_PASSWORD}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY}
- docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} .
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
# For tagged commits, automatically build a tagged image.
build:tag:
<<: *dind_template
only: [tags]
script:
- echo "${CI_REGISTRY_PASSWORD}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY}
- docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} .
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
The above example is a great improvement over the simple, hash-based tagging
strategy. Once you release version, say 1.3.7-alpha
, the CI will automatically
deploy the tagged image to your registry. This ensures that none of the images
are overwritten. The manual option to build hash-based images avoids cluttering
the registry. This strategy clearly defines a policy when to tag an image:
Always when you tag a git commit.
However, there are also disadvantages to this method. It does not create
latest
images. You can create a git tag called latest
, but this tag is
intended to be immutable, i.e., the tag should always point to the same commit.
That’s not we want for the latest tag. Tags in the docker world are mutable. If
only Git had something like a
rolling reference pointer which is updated when you add a new commit. Oh, wait.
Use release branches
Git branches are references that point to the latest commit in a sequence of
commits. It is common to use release branches to handle different
released versions. Assume we want to have the following docker image tags:
latest
, 3
, 3.7
and3.7.2
. For this example, assume that version 3.7.2
is the latest version. This means all of these docker tags should
point to the same image.
To achieve the given task, we need to map this structure into Git
branches. We can build on top of the previous tag-based solution. Once a Git
commit is tagged with a full version identifier such as 3.7.2
or
3.7.2-alpha
, we never want to update that tag. So all
we need are branches for latest
, major version and minor version. I’ll call
them release/latest
, release/3
and
release/3.7
, respectively. I have added a prefix to avoid name
collisions.
Images with a full version identifiers (e.g. 3.7.2
) are build via the tag-based solution; all
other images are build from release branches.
stages:
- build
- deploy
# ... continuation of above snipped
# For commits on the release branches, automatically tag the image with the
# release name
tag:release_branch:
<<: *dind_template
stage: deploy
only:
- /^release\/.*$/
environment:
name: $CI_COMMIT_REF_NAME
url: https://url/to/your/registry
script:
# Pull image with full version identifier
- export UNIQUE_TAG=$(git describe)
- echo "${CI_REGISTRY_PASSWORD}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY}
- docker pull ${CI_REGISTRY_IMAGE}:${UNIQUE_TAG}
- docker tag ${CI_REGISTRY_IMAGE}:${UNIQUE_TAG} ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME#release/}
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME#release/}
Example
I have prepared an example repository which implements the solution illustrated above. The images are simple “Hello World”-applications.
This might also interest you