Managing Alpine Linux Based Container Images With Renovate
With it's default configuration, Renovate does a great job of managing container
image tag updates. One limitation is that for updates that include an OS in the
tag, for example the Alpine Linux version in python:3.11.3-alpine3.17
,
Renovate will only update to Python image tags matching that version, which may
limit updates, and doesn't identify any OS updates. This post details how to
configure Renovate to work around that limitation for Alpine images.
This post assumes you have a working knowledge of Renovate and will cover some more advanced topics. You can find more introductory information in the Renovate docs.
Docker Versioning Overview #
Container image tag versions can be wildly different, sometimes chaotic, but
there are some general conventions. Renovate tries its best to leverage them
with docker
versioning, which is the default for all container image tags.
Renovate treats any SemVer-like initial number as the "version", and it will maintain the "version" with the given specificity, so:
node:18
will be updated tonode:19
node:18.16
will be updated tonode:18.17
node:18.16.0
will be updated tonode:18.16.1
Anything after the first hyphen in an image tag is treated as the
"compatibility" indicator. Renovate will only identify updates that maintain
this "compatibility". So node:18.16.0-alpine
will be updated to another
-alpine
image, e.g. node:18.16.1-alpine
.
Complete details are available in the Renovate docs.
Docker Versioning and OS Versions #
In most cases maintaining image "compatibility" is desired, as you wouldn't want
python:3.11.3-slim-bullseye
updated to python:3.11.4-bullseye
or
python:3.11.4-alpine
since they're completely different base images.
This can also be a challenge, though, in cases where the OS version changes. For
example python:3.11.3-alpine
will be updated to python:3.11.4-alpine
, but
what if you're using a specific OS version, like python:3.11.3-alpine3.16
? You
might notice that python
updates stop appearing even though new python
images are released. In this case the Python v3.11.3 image was the last version
released on Alpine 3.16, so Renovate doesn't recognize any newer image tags that
maintain "compatibility", and there are no further updates.
The other challenge is that images with updated OS versions are not identified
as updates, so you might not know that Alpine 3.17 images were available
starting with Python 3.7.15, or Alpine 3.18 images were available starting with
Python 3.7.16. The same issue exists with other OSs, including Debian and
Ubuntu. If you're using python:3.11.4-bullseye
, Renovate will not identify an
update to python:3.11.4-bookworm
.
Why Track OS Versions #
Given the issues identified above, why would someone want to track specific OS versions if it can prevent future updates? As with almost everything in software, different use cases drive different solutions.
As an example from Docker Hub, the following tags all point to the latest
python
image: alpine
, 3-alpine
, 3.11-alpine
, 3.11.4-alpine
,
3.11.4-alpine3.18
. If the uses case is fairly generic, e.g. a Python-based
linting tool, it may not be overly sensitive to Python or Alpine changes, so
using python:alpine
may be appropriate. For something like the base image for
a containerized application, it's best to be as specific as possible. This
causes Renovate to identify any changes - application or OS - and make the
appropriate update, initiating a CI pipeline. This ensures any change is tested
before being merged into the codebase.
The Workaround #
The way to work around the Renovate constraints is to keep the docker
manager
for the docker
versioning, and add a
regex
manager to setup
a separate versioning mechanism for the Alpine changes. The regex
manager
allows a fully customized Renovate configuration to manage dependencies that the
other managers do not cover, and is the only way to have Renovate version the
same dependency multiple ways. It also requires more complex configuration,
which is detailed below.
Thanks to the Renovate team for their help sorting out this solution.
Image Tag Format #
As noted above, there is not strict versioning in container image tags, so investigating tags I was concerned about tracking revealed the following list:
docker:23.0.6-alpine3.17
docker:23.0.6-dind-alpine3.17
node:18.16.0-alpine3.17
python:3.11.4-alpine3.17
mcr.microsoft.com/powershell:7.3-alpine-3.17
The example below covers these cases, but other image tags may drive Renovate configuration tweaks.
Renovate Configuration #
The following section details the configuration of the regex
manager to
version the Alpine OS. For any values that are regular expressions, they're
specified as the JSON string representation of the regular expression, so any
\
must be escaped as \\
.
The fileMatch
property is required and identifies regular expressions to match files to check.
In this case we'll set it to ["\\.gitlab-ci\\.yml$", "Dockerfile"]
, which will
check all GitLab CI files (assuming they end in .gitlab-ci.yml
) and all
Dockerfiles (which matches Dockerfile
, Dockerfile.<value>
, or
<value>.Dockerfile
). This could be updated with any other required files.
The
matchStrings
property is required and identifies regular expressions to match the
dependencies that the regex manager should manage (in this case container
images). To keep it as flexible as possible it will have a depName
capture
group with the dependency name, and a currentValue
capture group with the
version. So, for the image and tag that will be
(?<depName>[\\S]+):(?<currentValue>[\\S]+)
, using non-whitespace (\S
) to
allow any valid characters for both.
To constrain the matchStrings
further, the regular expression is updated with
the keywords indicating a container image. For Dockerfile
s, that's simply
FROM <image_name>
. For .gitlab-ci.yml
files, the following cases are
covered:
image:
name: <image_name>
image: <image_name>
services:
- <image_name>
Note this does not cover all variations of container images in services
(it's
missing at least two), but includes all that I am currently using. See the
GitLab services
docs for
additional details.
To cover these cases we'll preface the previous matchString
with each of them
OR'd together, which results in:
"matchStrings": [
"(?:image:\\s+name:\\s*|image:\\s*|services:\\s+-\\s+|FROM\\s+)(?<depName>[\\S]+):(?<currentValue>[\\S]+)"
]
See here for a full breakdown of this regular expression, with examples.
The
datasourceTemplate
property specifies the datasource type, which is docker
.
For versioning image tags, we're specifically trying to override the default
docker
versioning behavior, so we'll specify a
versioningTemplate
property telling Renovate how to extract data from the currentValue
(i.e. the
image tag). The default behavior is decribed above, which we're going to reverse
to track the Alpine versions. Taking the previous python
example, the tag
3.11.4-alpine3.17
will have compatibility
= 3.11.4-alpine
, major
version
= 3
, and minor
version = 17
. Using the data above on different tags
formats gives:
"versioningTemplate": "regex:^(?<compatibility>[\\S]*\\d+\\.\\d+(?:\\.\\d+)?(?:[\\S]*)?-alpine-?)(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?$"
See here for a full breakdown of this regular expression, with examples.
The final Renovate config for this regex manager is shown below:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "docker:enableMajor"],
"regexManagers": [
{
"description": "Manage Alpine OS versions in container image tags",
"fileMatch": ["\\.gitlab-ci\\.yml$", "Dockerfile"],
"matchStrings": [
"(?:image:\\s+name:\\s*|image:\\s*|services:\\s+-\\s+|FROM\\s+)(?<depName>[\\S]+):(?<currentValue>[\\S]+)"
],
"versioningTemplate": "regex:^(?<compatibility>[\\S]*\\d+\\.\\d+(?:\\.\\d+)?(?:[\\S]*)?-alpine-?)(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?$",
"datasourceTemplate": "docker"
}
]
}
The Limitations #
In general, the default docker
versioning and regex
versioning work
independently, and should identify updates for either or both, as applicable.
The one known limitation is when both identify a major update, or both identify
a minor/patch update. This can be seen with images like
python:3.11.3-alpine3.17
, which has a minor version update to Python 3.11.4
and also a "minor" version update to Alpine 3.18 (since major/minor are defined
this way in the custom versioning). So, Renovate sees two python-3.x
updates,
and one of them is ignored (in the Renovate logs you'll see the message
"Ignoring upgrade collision"). With this example the python:3.11.4-alpine3.17
update alone is processed, and once it's merged the python:3.11.4-alpine3.18
update is processed.