Automating Container Builds with GitHub Actions for Upstream Changes
Automating container builds with GitHub Actions when the upstream image changes.
Keeping your container images up to date is crucial for security and performance. Whether you’re running a homelab or managing production workloads, you want to ensure that your containers are built with the latest upstream changes. However, manually keeping track of updates and rebuilding containers can be tedious.
I use Tailscale to easily access services I run at home, or to easily connect to a lab environment in Azure from wherever I may be. To make this easier, I extended the Tailscale container image so I could deploy it as a subnet router and inject the address ranges that I needed advertised to my Tailnet. This made my setup more resilient, but I still had to make sure my container was using the latest version of Tailscale to make sure I was secure and prevent possible issues.
Until recently, I had a simple GitHub Actions workflow that would build and push to Docker Hub, whenever I remembered to check for updates (which I didn’t do very regularly) to the upstream Tailscale image.
In this post, I’ll walk through how I use a GitHub Actions workflow to automatically check for updates to an upstream Docker image, and trigger a build and push only if a new version is available. This kind of automation saves time, reduces human error, and keeps my environment reliably up to date.
Why Automate Container Builds?
If you’re maintaining a custom container that wraps or extends functionality from an upstream project (like Tailscale, Nginx, or Node.js), you’ll often want to:
- Track upstream releases.
- Rebuild your container with the new version.
- Push the updated image to Docker Hub or a private registry.
- Optionally commit a version change back to your repo for tracking.
Doing all this manually is inefficient. Automating this with GitHub Actions ensures consistency and frees you to focus on more important tasks.
The Workflow
Here’s a breakdown of the GitHub Actions workflow I use. It:
- Runs every 3 days or on manual trigger.
- Checks the latest stable tag of the upstream container.
- Compares it against images already published.
- If a newer version is available, builds and pushes multi-arch images.
- Commits a version file update for visibility.
Secrets You Will Need
Before you can use this workflow, you’ll need to set up a few secrets in your GitHub repository to allow the workflow to authenticate with Docker Hub. The workflow uses a Docker Hub username and an access token for authentication.
DOCKERHUB_USERNAME
: Your Docker Hub username.DOCKERHUB_TOKEN
: A Docker Hub access token with write permissions.
To set these secrets, go to your GitHub repository, click on Settings, then Secrets and variables > Actions. Click on New repository secret to add each secret.
Workflow Breakdown
Let’s walk through what each section of the workflow does and how it contributes to the build and update process.
1. Triggers
1
2
3
4
on:
schedule:
- cron: '0 0 */3 * *'
workflow_dispatch:
schedule
: Runs the workflow automatically every 3 days.workflow_dispatch
: Allows you to manually trigger the workflow from the GitHub UI.
2. Permissions
1
2
permissions:
contents: write
This grants the workflow permission to make commits to the repository, which is necessary so that the upstream-version.txt
file can be updated and pushed to the repo if there is a new upstream container version.
3. Job: check-and-build
The entire automation is contained in a single job that runs using the latest Ubuntu GitHub Actions runner.
Step: Checkout Repo
1
2
3
4
5
6
7
jobs:
check-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
This pulls down the repository code so that subsequent steps can read/write files like upstream-version.txt
and the Dockerfile
.
Step: Get Latest Upstream Version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: Get latest Tailscale stable version
id: tailscale
run: |
VERSION=$(curl -s https://registry.hub.docker.com/v2/repositories/tailscale/tailscale/tags/?page_size=100 \
| jq -r '.results[].name' \
| grep -E '^v[0-9]+\.[0-9]+(\.[0-9]+)?$' \
| sort -V \
| tail -n1)
echo "Latest stable version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Debug version
run: |
echo "Detected Tailscale version: ${{ steps.tailscale.outputs.version }}"
- Uses Docker Hub’s API to fetch the latest stable tag from an upstream image.
- Outputs the latest version as
steps.upstream.outputs.version
.
Step: Check for Existing Image
1
2
3
4
5
6
7
8
9
10
- name: Check if image with version already exists
id: check
run: |
if docker manifest inspect ${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:${{ steps.tailscale.outputs.version }} > /dev/null 2>&1; then
echo "Image already exists. Skipping build."
echo "build_needed=false" >> $GITHUB_OUTPUT
else
echo "New version. Proceeding to build."
echo "build_needed=true" >> $GITHUB_OUTPUT
fi
- Uses
docker manifest inspect
to check whether a container image for this version already exists in your Docker Hub repo. - Sets a flag
build_needed
totrue
orfalse
.
Conditional Build Steps
The following steps are only run if a new version is detected, resulting in build_needed
being set to true
.
- Set up emulation support (QEMU) for building multi-architecture containers.
- Set up Docker Buildx for advanced build features.
- Log into Docker Hub using credentials stored in GitHub Secrets.
- Build and push the container using the new version as a tag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- name: Set up QEMU
if: steps.check.outputs.build_needed == 'true'
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
if: steps.check.outputs.build_needed == 'true'
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: steps.check.outputs.build_needed == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push multi-arch container
if: steps.check.outputs.build_needed == 'true'
uses: docker/build-push-action@v3
with:
context: ./docker
build-args: |
TAILSCALE_TAG=${{ steps.tailscale.outputs.version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:latest
${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:${{ steps.tailscale.outputs.version }}
Step: Commit Version Update
1
2
3
4
5
6
7
8
9
- name: Commit updated version file
if: steps.check.outputs.build_needed == 'true'
run: |
echo "${{ steps.tailscale.outputs.version }}" > tailscale-version.txt
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add tailscale-version.txt
git commit -m "Update Tailscale version to ${{ steps.tailscale.outputs.version }}"
git push
This writes the new version to a file, commits it, and pushes it to your GitHub repo. This gives you a version history and a way to confirm that the build occurred.
The YAML Workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
name: Auto Build & Publish Tailscale Container
on:
schedule:
- cron: '0 0 */3 * *' # Every 3 days at 00:00 UTC
workflow_dispatch:
permissions:
contents: write # Needed to commit version file
jobs:
check-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Get latest Tailscale stable version
id: tailscale
run: |
VERSION=$(curl -s https://registry.hub.docker.com/v2/repositories/tailscale/tailscale/tags/?page_size=100 \
| jq -r '.results[].name' \
| grep -E '^v[0-9]+\.[0-9]+(\.[0-9]+)?$' \
| sort -V \
| tail -n1)
echo "Latest stable version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Debug version
run: |
echo "Detected Tailscale version: ${{ steps.tailscale.outputs.version }}"
- name: Check if image with version already exists
id: check
run: |
if docker manifest inspect ${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:${{ steps.tailscale.outputs.version }} > /dev/null 2>&1; then
echo "Image already exists. Skipping build."
echo "build_needed=false" >> $GITHUB_OUTPUT
else
echo "New version. Proceeding to build."
echo "build_needed=true" >> $GITHUB_OUTPUT
fi
- name: Set up QEMU
if: steps.check.outputs.build_needed == 'true'
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
if: steps.check.outputs.build_needed == 'true'
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: steps.check.outputs.build_needed == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push multi-arch container
if: steps.check.outputs.build_needed == 'true'
uses: docker/build-push-action@v3
with:
context: ./docker
build-args: |
TAILSCALE_TAG=${{ steps.tailscale.outputs.version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:latest
${{ secrets.DOCKERHUB_USERNAME }}/tailscale-sr:${{ steps.tailscale.outputs.version }}
- name: Commit updated version file
if: steps.check.outputs.build_needed == 'true'
run: |
echo "${{ steps.tailscale.outputs.version }}" > tailscale-version.txt
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add tailscale-version.txt
git commit -m "Update Tailscale version to ${{ steps.tailscale.outputs.version }}"
git push
Final Thoughts
Automating container builds with GitHub Actions is a powerful way to keep your images up to date without manual intervention. This workflow can be adapted for any upstream project, and you can customize it further based on your needs.
- Consider adding notifications (e.g., Slack, email) to alert you when a new version is built.
- You can also extend the workflow to run tests against the new image before pushing it.
- Explore other GitHub Actions to enhance your CI/CD pipeline.
If you’ve built similar automation’s or have tips for improving this setup, let me know—I’d love to hear how others are tackling this.