Go Mercedes!

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 only updates 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 covers 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 maintains the "version" with the given specificity, so:

  • node:18 is updated to node:19
  • node:18.16 is updated to node:18.17
  • node:18.16.0 is updated to node:18.16.1

Anything after the first hyphen in an image tag is treated as the "compatibility" indicator. Renovate only identifies updates that maintain this "compatibility." So node:18.16.0-alpine is updated to another -alpine image, for example 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 is 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 does not identify an update to python:3.11.4-bookworm.

Why track OS versions #

Given the previously identified issues, 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, for example 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 in the preceding example, there is not strict versioning in container image tags, so investigating tags of interest in 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 set it to ["\\.gitlab-ci\\.yml$", "Dockerfile"], which checks 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 has a depName capture group with the dependency name, and a currentValue capture group with the version. So, for the image and tag that is (?<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 Dockerfiles, 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 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 data source type, which is docker.

For versioning image tags, specifically trying to override the default docker versioning behavior, specify a versioningTemplate property telling Renovate how to extract data from the currentValue (that is, the image tag). The default behavior is described previously, which is going to be reversed to track the Alpine versions. Taking the previous python example, the tag 3.11.4-alpine3.17 has compatibility = 3.11.4-alpine, major version = 3, and minor version = 17. Using the previous data 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 configuration 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.