Cover Image: Any docker images to be updated?
Photo by Timelab Pro on Unsplash

Any docker images to be updated?

Tomas Norre • February 2, 2022

devops

Keeping docker images up to date, can be a challenge, I'm not talking about the images you maintain yourself, but the public ones that you're relying on. These needs to be updated from time to time too, but how do you figure out when? How can you monitor this?

Disclaimer: This post is heavily inspired by https://mlohr.com/check-for-docker-image-updates/

I don't know which CI tool you're using, I'm using GitHub Actions, but there ideas below can be applied to GitLab, Circle CI, Jenkins etc.

The script from the blog post, see above, makes it possible to compare the image present, and the newest version of it. As I'm lazy and want this to be part of my CI I have adjusted it a little.

Let's start from the minimum.

Running Local

The script from https://mlohr.com/check-for-docker-image-updates/ can be used to scan locally.

You can save it as docker-image-update-check.sh and chmod +x docker-image-update-check.sh then you can execute it like the example usage in the script. This requires that the gitlab/gitlab-ce-image is pulled locally, otherwise it will not detect a local digest and therefore it will always say that an update is needed.

#!/bin/bash
# Example usage:
# ./docker-image-update-check.sh gitlab/gitlab-ce update-gitlab.sh

IMAGE="$1"
COMMAND="$2"

echo "Fetching Docker Hub token..."
token=$(curl --silent "https://auth.docker.io/token?scope=repository:$IMAGE:pull&service=registry.docker.io" | jq -r '.token')

echo -n "Fetching remote digest... "
digest=$(curl --silent -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    -H "Authorization: Bearer $token" \
    "https://registry.hub.docker.com/v2/$IMAGE/manifests/latest" | jq -r '.config.digest')
echo "$digest"

echo -n "Fetching local digest...  "
local_digest=$(docker images -q --no-trunc $IMAGE:latest)
echo "$local_digest"

if [ "$digest" != "$local_digest" ] ; then
    echo "Update available. Executing update command..."
    ($COMMAND)
else
    echo "Already up to date. Nothing to do."
fi

Prepare for CI

If we do small adjustments in the script, taking an image including version as input instead, I have also removed the COMMAND param as not needed for my case, we will get a script that looks like this:

#!/bin/bash

IMAGE_WITHOUT_VERSION=$(echo $1 | cut -f 1 -d ':')
IMAGE_WITH_VERSION="$1"

echo "Fetching Docker Image..."
docker pull $IMAGE_WITH_VERSION

echo "Fetching Docker Hub token..."
token=$(curl --silent "https://auth.docker.io/token?scope=repository:$IMAGE_WITHOUT_VERSION:pull&service=registry.docker.io" | jq -r '.token')

echo -n "Fetching remote digest... "
digest=$(curl --silent -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  -H "Authorization: Bearer $token" \
  "https://registry.hub.docker.com/v2/$IMAGE_WITHOUT_VERSION/manifests/latest" | jq -r '.config.digest')
echo "$digest"

echo -n "Fetching local digest...  "
local_digest=$(docker images -q --no-trunc $IMAGE_WITHOUT_VERSION)
echo "$local_digest"

if [ "$digest" != "$local_digest" ] ; then
  echo "Update available. Please update..!"
  exit 1
else
  echo "Already up to date. Nothing to do."
fi
exit 0

This update will give us the possibility to run the script like follows: ./docker-image-update-check.sh grafana/grafana:8.3.3 and it will tell us that an update is present as grafana/grafana:8.3.4 is released. It will not tell us which update is present, but only that one is present.

Now we have a script that can scan one image, but adding it to our CI, there is most likely more images to be scanned, so we need to wrap this with some small scripts.

There are multiple ways to boot your images, Docker-compose, docker swarm, kubernetes etc. I use docker-compose, and I have a small script that converts my image list to json, which I can use in my GitHub Action Matrix.

First we need a list of images present in our setup. I do this with searching in the repositories docker-compose.yml-files

find . -name 'docker-compose*' -type f | xargs grep 'image:' | awk '{print $3}' | sed "s/'//g" | sort -n | uniq > images.txt

Now we have an images.txt-file that the next script needs. This script that produces json from the images.txt-file.

#!/bin/bash

# removes already present images.json if any
rm images.json

printf '[' >> images.json
while read -r line; do
  printf "\"%s\"," "$line" >> "images.json";
done < images.txt
printf ']' >> images.json

# Remove the tailing `,` at the last entry in file
sed -i 's/,]/]/' images.json

# echos the file, for use later in GitHub Action
cat images.json

Combine it to a GitHub Action

At this point we are ready to combine this all into a GitHub Action.

name: Check Docker Image Updates

on:
  push:
    branches:
      - "**"
  schedule:
    - cron: '30 0 * * *'

jobs:
  # We create the image.json and outputs it as matrix-public
  provide_image_json:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Fetch Docker Images (Public)
        run: |
          find . -name 'docker-compose*' -type f | xargs grep 'image:' | awk '{print $3}' | sed "s/'//g" | sort -n | uniq > images.txt

      - id: set-matrix-public
        run: echo "::set-output name=matrix-public::$(./scripts/pipeline/text-to-json-public.sh)"

    outputs:
      matrix-public: ${{ steps.set-matrix-public.outputs.matrix-public }}

  # We are iterating over all the image in the ${{ matrix.image }} 
  update-to-date:
    needs: provide_image_json

    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        image: ${{fromJson(needs.provide_image_json.outputs.matrix-public)}}

    steps:
      - uses: actions/checkout@v2
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v1

      - name: Check if update exists for image ${{ matrix.image }}
        env:
          IMAGE: ${{ matrix.image }}
        run: |
          IMAGE_WITHOUT_VERSION=$(echo $IMAGE | cut -f 1 -d ':')
          IMAGE_WITH_VERSION="$IMAGE"
          docker pull $IMAGE_WITH_VERSION
          echo "Fetching Docker Hub token..."
          token=$(curl --silent "https://auth.docker.io/token?scope=repository:$IMAGE_WITHOUT_VERSION:pull&service=registry.docker.io" | jq -r '.token')
          echo -n "Fetching remote digest... "
          digest=$(curl --silent -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
            -H "Authorization: Bearer $token" \
            "https://registry.hub.docker.com/v2/$IMAGE_WITHOUT_VERSION/manifests/latest" | jq -r '.config.digest')
          echo "$digest"
          echo -n "Fetching local digest...  "
          local_digest=$(docker images -q --no-trunc $IMAGE_WITHOUT_VERSION)
          echo "$local_digest"
          if [ "$digest" != "$local_digest" ] ; then
            echo "Update available. Please update..!"
            exit 1
          else
            echo "Already up to date. Nothing to do."
          fi
          exit 0

Now we have a GitHub Action that iterates over all images used in the repository, declared in docker-compose.yml files.

This now gives me an overview of images that needs to be updated. In this case no one, the job will fail if updates are needed. GitHub Action Updates

Conclusion

There might be better ways to do this, but this is solution that I came up with for now. So if you have input and suggestions please reach out, will be happy to hear about it.

Additional Info

If you are running this with private images/docker registry, you will need to login and verify of course. Running this on private repository, will of course eat up quite some build minutes, as all images are pull on every single run. It might be possible to improve this with GitHub Action caching or inspecting the images without pulling them.

This is a start, and there are room for improvements.

If you find any typos or incorrect information, please reach out on GitHub so that we can have the mistake corrected.