Repository navigation
ci: pull CI and base images from a GHCR mirror instead of Docker Hub - #801
Merged
Merged
Conversation
The merge-group run of release PR #800 failed because Docker Hub rate-limited anonymous pulls (429 toomanyrequests): the image builds (oven/bun base, the docker/dockerfile frontend, moby/buildkit for setup-buildx) and the service containers and `docker run` steps of packed, grids and integration. A release pulls these images about 27 x 2 times, so retries cannot fix it. Every Docker Hub image that CI, the release workflows or the image builds use is now pinned by digest in .github/mirror-images.txt and copied, manifest list unchanged, to the public ghcr.io/k2b-dev/mirror by .github/mirror-images.sh. Service containers, BuildKit and the Dockerfiles name the mirror at the same digest, so provenance and reproducibility stay intact. Steps and test fixtures that start an image by its Docker Hub name get it from pull-images.sh, which pulls the mirrored digest and tags it locally with that name. CI's new `mirror` job runs the copy before every job that pulls an image, so a pull request that adds or bumps an image mirrors it first; otherwise it only verifies that each digest is present and anonymously pullable. The same workflow runs weekly and on demand. The `mirror-images` check fails when a workflow or Dockerfile would pull from Docker Hub, when a mirror reference does not carry the listed digest, and when a list entry is unused.
The Assistant artifact suite starts `postgres:17-alpine` by name. It found that tag only because the integration service container used the same name; with the service on the mirror reference it would pull from Docker Hub again. The integration pull steps of ci.yml and nightly.yml now pull it, and the suite's rsql image, through pull-images.sh. BuildKit's default SBOM generator, docker/buildkit-syft-scanner, comes from Docker Hub, and every release image build pulls it. The builds now name the mirrored generator. The mirror-images check flags an SBOM attestation without a mirrored generator and `docker://` steps that point to Docker Hub. mirror-images.sh decided presence by the mirror's tag, which another branch can move, so it copied from Docker Hub again for digests the mirror already served. It now asks GHCR for the pinned digest without credentials, which is how consumers pull. It takes the copied digest from the push, since GHCR can answer a fresh tag with stale data. It retries registry requests, keeps going after a failed image, and copies single manifests unchanged. A failed login fails only a run that has to copy.
ValentinKolb
enabled auto-merge
October 9, 2026 22:47
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 9, 2026
# Conflicts: # .github/workflows/ci.yml
main added .github/poppler.sh, which pulls backplane/pdf from Docker Hub by digest; pull-images.sh refuses unlisted Docker Hub images. The image is now listed in .github/mirror-images.txt and poppler.sh names its mirror reference. The mirror-images check also reads shell scripts, so that reference counts as a use and must carry the listed digest.
ValentinKolb
enabled auto-merge
October 10, 2026 07:33
Matches the date that the queued Assistant skills change sets on build-and-deploy.md, so both merge without a conflict.
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 10, 2026
ValentinKolb
enabled auto-merge
October 10, 2026 08:12
This was referenced Oct 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outcome
CI, the nightly and release workflows and the image builds no longer pull anything from Docker Hub. Every Docker Hub image they use is pinned by digest in
.github/mirror-images.txtand pulled from the public mirrorghcr.io/k2b-dev/mirror/<name>:<tag>@sha256:…, which serves the same digest, so provenance and reproducibility stay intact.Root cause
The merge-group runs of release PR #800 failed twice on Docker Hub's anonymous pull limit (
429 toomanyrequests):Both times
images,grids (1-3),integrationandpackedfailed: the image builds (oven/bunbase images, thedocker/dockerfilefrontend,moby/buildkitfor setup-buildx, BuildKit's default SBOM generatordocker/buildkit-syft-scanner) and the service containers anddocker runsteps. A release pulls these images roughly 27 x 2 times, so retries cannot fix it.How it works
.github/mirror-images.txtlists every Docker Hub image as<name>:<tag>@sha256:<digest>(12 entries)..github/mirror-images.shcopies each listed digest the mirror does not serve yet, manifest list unchanged (docker buildx imagetools create), and verifies that every digest is pullable without credentials (anonymous GHCR token + manifest HEAD). Registry requests are retried, a failed image does not stop the others, and the script is idempotent and resumable..github/workflows/mirror-images.ymlruns the script;ci.ymlcalls it as a newmirrorjob that every job pulling an imageneeds, so a PR that adds or bumps an image mirrors it first. The same workflow runs weekly, on demand and onmainpushes that change the list.# syntax=/FROMlines of every Dockerfile name the mirror reference at the listed digest.docker run nats:2.14.3-alpine, the Files Filegate suites, the Assistant artifact suite's Postgres, the packed runtime check) get it from.github/pull-images.sh, which pulls the mirrored digest and tags it locally with the Docker Hub name; it refuses an unlisted Docker Hub image.scripts/checks/mirror-images.ts(+ tests) fails when a workflow or Dockerfile would pull a Docker Hub image directly (including the default SBOM generator anddocker://steps), when a mirror reference carries a different digest than the list, and when a list entry is unused.Bootstrap order (one time)
mirrorjob copies all 12 digests into 9 new packages withGITHUB_TOKEN(packages: write). If Docker Hub answers 429 partway, re-running only themirrorjob continues where it stopped.mirrorjob:Status: done. The first
mirrorrun of this PR (run 38001105385) copied all 12 digests without a Docker Hub 429, and GHCR created the packages publicly readable, so no visibility change was needed. All 12 digests answer an anonymous token request and manifest HEAD with 200.After that, a run only verifies the mirror and never touches Docker Hub.
Docs
contributing/testing.md: new section "Where CI images come from" (how images are consumed, themirrorjob, the check, how to add or bump an image, fork PRs, trying it against a local registry).contributing/release-process.md: release image builds take base images, frontend, BuildKit and SBOM generator from the mirror.operations/build-and-deploy.md: building the monorepo Dockerfiles from source needsghcr.io, no Docker Hub or GitHub login.Verification
bun run check: 24/24 rules pass, including the newmirror-imagesrule.bun test scripts/checks/mirror-images.test.ts: 6 pass.git diff --checkclean;bash -non both scripts.mirrorjob, all 12 digests are anonymously pullable (token 200, manifest HEAD 200).Residual risks
ghcr.ioinstead of Docker Hub for these images; a GHCR outage blocks them the same way.mirrorjob and the weekly run loudly rather than silently falling back to Docker Hub.mirrorjob fails until a maintainer runsMirror imageson a branch of this repository with the same list.mirror-imagescheck cannot see which job runs a test file that starts its own container; such an image must be added to the pull steps inci.ymlandnightly.ymlby hand (documented).