Docker images since 2015

PROBLEMA tag is not evidence.

I have published container images since 2015. They range from Ubuntu bases to Android, Node, Cordova and Ionic stacks. They are pulled onto machines I do not choose, into builds I never see.

The puller's architecture is not the publisher's to pick. Development machines are arm64; most CI runners are amd64; some hardware is still arm/v7. An image built for one of them fails on the others with an exec format error.

Pinning a base image to a digest freezes the build. It also freezes every vulnerability that digest carried. The upstream fix ships and does not arrive until somebody remembers to bump the pin.

A tag is a name. latest does not say when it last moved. A user cannot tell a day-old image from a year-old one.

APPROACHTrust is a pipeline outcome.

Each of the five repositories carries one workflow file, .github/workflows/docker.yml. All five build the image and push it. Only docker-base lints the Dockerfile, tests the image and signs it. That repository and docker-android also scan the published tag, after the push.

One job, every platform.

Buildx and QEMU build all platforms in a single job and push one manifest list. The registry then serves each puller the image for its own architecture. The cost is wall time. Emulated architectures build slower than the native one, and the job takes as long as the slowest. Splitting the build per architecture and merging the digests with imagetools buys that time back. It costs a second job to maintain. One of the five gets the full set. docker-base publishes linux/amd64, linux/arm64/v8, linux/arm/v7, linux/s390x and linux/ppc64le. The other four publish linux/amd64 only. docker-android installs the Android command-line tools, which ship for that platform alone. docker-android-nodejs, docker-cordova and docker-ionic each build FROM the image before them, so the constraint carries down.

Fresh bases, rebuilt daily.

The Dockerfiles do not pin a digest. docker-base tracks an Ubuntu LTS tag. A cron line rebuilds each image daily. Three fire at 10:00 UTC; docker-ionic at 12:00 and docker-cordova at 15:00. The other four build FROM another image with no tag. That resolves to latest, which the daily run moves. Pinning is reproducible and defers every upstream fix until somebody bumps it. This is the inverse trade: take the churn, and pay for it with verification. An upstream change can break a published image on a morning nobody planned. The gates below are the answer to that, and only docker-base runs all of them.

The base image is tested.

Testing the image is docker-base's decision; the other four repositories do not carry it. Hadolint lints the Dockerfile on every run. On a pull request the image is built for linux/amd64, not pushed, and handed to Container Structure Tests. They assert file presence, image metadata, environment variables, exposed ports and command output. One test runs cat /etc/os-release and compares it against the Ubuntu release the test file names. Docker Scout then compares the pull request against the deployed image and fails on a critical or high finding. On a push the order reverses. The image is pushed, then signed with Cosign and scanned. Scout uploads its findings to code scanning. The build also attaches an SBOM and a provenance attestation. docker-android runs the same post-push scan and nothing else. The cost is a test file, and it changes whenever the image legitimately changes. The upstream Container Structure Test project is in maintenance mode; it still catches the drift it is pointed at.

Tags are dates, not versions.

The daily run moves two tags on every image: latest and nightly. A release is a git tag of the shape v2025.08.3, published under the same name. The third number is a sequence within the month, not a day. Branch and pull-request builds publish their own tags. The registry therefore holds more tags than this policy describes. The release tag gives the year and the month; latest and nightly are dated by the rebuild. A semantic version would answer a different question; a base image has no API to break. The cost is that the CalVer tag carries no compatibility signal. A change that breaks a build arrives under a tag that looks like every other tag. No tag is ever deleted, because deleting one breaks whatever was pinned to it.

RESULTThe gates cover one image.

Five repositories publish these images: docker-base, docker-android, docker-android-nodejs, docker-cordova and docker-ionic. All five are public, under the Beevelop organisation on GitHub. The oldest was created in August 2015. One of the five runs all the gates. A gate on the base does not test what is built on top of it.

The 31 public images have been pulled 14,970,732 times, measured on 6 August 2026.

Nothing here records an unpinned base breaking a published image. That decision is stated with its cost; it is not proven by an incident.

Read .github/workflows/docker.yml in docker-base, then the same file in the other four. The schedule, the gates and the tag policy are in it. The Actions tab of each repository shows whether the daily run still fires. None of it has to be taken on trust.