Using Dependabot to Maintain Docker Image Dependencies
One of the challenges in modern software development is keeping on top of your dependencies.
There are many tools out there to help you wrangle your dependencies; if you use GitHub, you have most likely come across Dependabot. This post isn’t an introduction to Dependabot. It presents a way to automatically maintain Docker image versions referenced by your infrastructure, without giving up control of when those updates happen.

Overview of Dependabot Docker
Dependabot already supports updating a large number of different types of dependencies, including DockerFiles. You just need to add a configuration and it will automatically raise pull requests when there is a new version of your base Docker image.
For example, I might have a Docker image built from the base image ubuntu:24.04
:
# ./DockerFile
FROM ubuntu:24.04
WORKDIR /app
# COPY files/install things, etc
When a new version of the ubuntu image is released, I get a Pull Request…
-FROM ubuntu:24.04
+FROM ubuntu:24.10
WORKDIR /app
# COPY files/install things, etc
I can review the Pull Request, read the release notes, check for any issues, and, if all is well, accept the new version of Ubuntu.
This all works out of the box, but having a clear understanding of this will help me explain the solution later.
The Problem
When you host a container application, such as in AWS Elastic Container Service (ECS) or Azure’s Container Apps you can either host your own image from your own repository (e.g. Elastic/Azure Container Registry), or reference an image from a public registry such as Docker Hub / AWS Public ECR.
If you are building your application into a Docker image and running that image in a container service, you won’t have this problem because you will be tagging the image and instructing the container service to use it when you deploy a new version.
For example, an abridged deployment might be:
- Container service running
MyApp:v1
- Build and publish
MyApp:v2
(e.g. docker build and push) - Instruct container service to run
MyApp:v2
(e.g. update via terraform) - Container service running
MyApp:v2
However, there are cases where you are running some public image in your container service. It might be nginx acting as a reverse proxy, or fluentbit in a sidecar configuration to provide advanced logging capabilities.
In these cases you have two choices as to how you tell your container service which image you would like:
- Use the
latest
tag, e.g.nginx:latest
- Use a specific version tag, e.g.
nginx:1.27-perl
Both have their pros and cons:
Approach | Pros | Cons |
---|---|---|
Latest tag | Low maintenance, automatic updates | Lack of control over updates |
Specific tag | Control over versions | Requires manual updates |
The tag you use will usually end up stored somewhere in your infrastructure as code. For example, for AWS ECS and Terraform you have it in your ECS Task Definition JSON:
"containerDefinitions": [{
"name": "nginx",
"image": "public.ecr.aws/nginx/nginx:latest",
"memory": 256,
"etc": "etc",
}]
If you are using the latest
tag approach, you lose control of when your application
takes an updated version of the image.
If there is a new latest
tag of nginx
and that contains a critical problem, next time a new ECS Task is deployed you
will get that problem in your application.
You won’t be able to rollback either, because it will still use the latest
image.
If you are using a specific version tag, like 1.27-perl
you no longer get
automatic updates. Instead, you have to remember
to check for updates to nginx
at regular intervals, and watch out for CVEs
that might place your application at risk, and then go in and manually change the
version number.
The benefit of Dependabot is that it just tells you when there’s a new version. Wouldn’t it be good if you could still reference a specific version tag, but also have Dependabot tell you when there is an update available via a Pull Request?
The Solution
To get the best of both worlds you need a way for Dependabot to monitor a specific version tag that is buried in your code. We already know Dependabot can maintain a base Docker image version.
I’ll continue the example assuming you are using AWS ECS and images from their public ECR. This should work fine for Azure and other Container Service Providers, as well as other Docker image Registries like DockerHub - so long as Dependabot supports the Image Registry.
First, we need to get the specific version tag out into a place where Dependabot can deal with it.
Fake Docker File
Start by creating a new folder for “3rd Party Images” and in there a folder for
each image tag. Then create a DockerFile
referencing a specific tag.
# Create a new folder for 3rd Party Images and an nginx sub folder
mkdir -p ./third-party-docker-images/nginx/
# Change to that new folder
cd ./third-party-docker-images/nginx/
# Create a DockerFile
echo "FROM public.ecr.aws/nginx/nginx:1.27-perl" > DockerFile
This will leave you with a DockerFile like so:
FROM public.ecr.aws/nginx/nginx:1.27-perl
This will be a “Fake DockerFile” who’s only purpose is to be monitored and updated by Dependabot. You don’t need to do anything else with it, but adding some comments or a readme file will help.
If you have other images, add another folder in ./third-party-docker-images/whatever
with another DockerFile
.
Configure Dependabot
Next configure Dependabot to keep an eye on a Fake DockerFile:
# ./.github/dependabot.yml
version: 2
updates:
# Update nginx third party image
- package-ecosystem: "docker"
directory: "/third-party-docker-images/nginx/"
schedule:
interval: "weekly"
# Lots of other dependencies to watch...
Whenever Dependabot runs it will raise a Pull Request if there is a newer
version of nginx
.
Wiring the Version
The final step is to get the image tag from the DockerFile so that you can reference it in your infrastructure as code.
Use whatever scripting language you are comfortable to read the file and get the image tag:
Some examples in bash and PowerShell are below:
Bash
# Read the first line of the file
contents=$(head -n 1 ./DockerFile)
# Get the full image tag
imagetag=$(echo $contents | sed -E "s/FROM (.*)/\1/g")
# Get just the version
version=$(echo $contents | sed -E "s/FROM.*:(.*)/\1/g")
PowerShell 7
# Read the first line of the file
$contents = Get-Content .\DockerFile -Head 1
# Get the full image tag
$imagetag = $contents.Split(" ")[1]
# Get just the version
$contents -match "FROM.*:(.*)"
$version = $Matches[1]
These are not fool proof ways to parse DockerFiles, but work for simple examples. Your mileage may vary.
If your Task Definition is using the full image tag from a public Docker Registry
then you can just use the $imagetag
variable and pass that into your Infrastructure
as code.
You just need some place in your deployment script to get the value of the image tag and pass it into terraform:
e.g.
export TF_VAR_nginx_image_tag="$imagetag"
#or
terraform plan -var 'nginx_image_tag=$imagetag'
And in your Task Definition JSON:
"containerDefinitions": [{
"name": "nginx",
"image": "${var.nginx_image_tag}",
"memory": 256,
"etc": "etc",
}]
Using a Pull Through Cache
The reason I have shown the $version
variable is because I use a
Pull Through Cache on ECR - just to ensure public images
don’t disappear on me - and that has a different repository address to the
public one:
"containerDefinitions": [{
"name": "nginx",
"image" : "${var.ecr_cache}/nginx/nginx:${var.nginx_version}",
"memory": 256,
"etc": "etc",
}]
In my Fake DockerFile I still reference AWS’s Public ECR. But in my Task Definition JSON I am referencing a variable pointing to my Pull Through Cache’s address (which is dynamically constructing it from other variables in terraform).
You could have
nginx/nginx:1.27-perl
in the$version
variable, if you prefer to do things that way, just update the script to parse theFROM ...
correctly.
Conclusion
You can’t always have your cake and eat it.
But, after been burned by a rouge latest
tag (it wasn’t from nginx
by the way) I needed a way to be able to take control of when new versions were
introduced into my application.
I needed to have a way to be able to revert them if they went rouge.
And at the same time, I wanted Dependabot to manage it alongside
all the other dependency updates that were going on.
Another benefit to this approach, is that it centralises the Docker image versions in a repository, so if you have multiple applications, each using the same version of a Docker image you can keep them in sync. If you don’t want to sync them, you can just have two separate DockerFiles - e.g. one to take minor updates, one to take nightly updates.
With the above changes I managed to get the best of both.