One package is to be published to the GitHub Container Registry:
ghcr.io/dip-develop/jaspr-build-image. It has never been built, so an
anonymous pull is refused with 403 and the package page answers 404; merging
to main is what publishes it (see Operator steps). Once
published it will hold two pinned toolchain base images that Jaspr sites build
FROM: the Dart flavour is multi-arch (linux/amd64 and linux/arm64), the
Flutter flavour is linux/amd64 only.
Nothing is deployed from this repository. Its only output is a toolchain image and nothing else — no site, no server, no host.
GitHub's release-download endpoint
(github.com/<org>/<repo>/releases/download/...) served HTTP 504 from GitHub's
own edge during the outage that motivated this repo: region fra,
negative-cached across all release assets, every asset of every repository.
Every consumer's docker build curled the Tailwind standalone CLI from that
endpoint and failed; rebuilding did not help while the negative cache lived. The
endpoint answered 200 again from this machine on 2026-11-09 — the trigger was
that outage, not a permanent property of the endpoint, and the design assumes it
can recur.
The download now happens once, at image-build time, into a cached GHCR layer, and the body is verified against a pinned sha256 before it is installed.
Neither tag exists on GHCR yet — the first publish happens when the PR that adds this repo merges. Once published:
| Tag | Base | Contents | Architectures | Planned consumers |
|---|---|---|---|---|
dart-3.13.4-jaspr-0.23.5-tailwind-4.3.0 |
dart:3.13.4 |
Dart SDK 3.13.4, jaspr_cli 0.23.5, Tailwind CSS CLI 4.3.0 |
linux/amd64, linux/arm64 |
dipdev-website, alteri-one |
flutter-3.44.9-jaspr-0.23.5-tailwind-4.3.0 |
dart:3.12.2 + Flutter 3.44.9 from the official archive |
Flutter SDK 3.44.9 (ships Dart 3.12.2), jaspr_cli 0.23.5, Tailwind CSS CLI 4.3.0 |
linux/amd64 |
Gewerber/gewerber-website, EVEFrontier-Club/frontend |
No consumer has been migrated yet. The consumer column is planned work
tracked in issue #2, one PR per repo: dipdev-website still builds from its own
build-base/, and alteri-one has no Dockerfile at all. Nothing in this repo
depends on that migration.
The two flavours cannot be merged. build_modules 5.x caps the Dart SDK at
<3.13.0, so a Flutter-based Jaspr site cannot run Dart 3.13.4 and must stay on
the Flutter line; a Dart-only site has no use for the Flutter SDK, which would
add gigabytes to every build for nothing.
They also differ in architecture. The Dart tag will be published for
linux/amd64 and linux/arm64; the Flutter tag for linux/amd64 only, because
Flutter publishes no linux-arm64 archive. Neither tag name encodes the
architecture, so the tag strings stay exactly as they are.
Flutter publishes no Linux archive for arm64: its Linux release manifest lists
749 releases and not one arm* artifact, and releases_linux_arm64.json
answers 404. An arm64 variant of this image would be an arm64 OS carrying an x64
SDK whose dart needs /lib64/ld-linux-x86-64.so.2, which the arm64 base does
not contain — a guaranteed failing RUN. The job will therefore publish
linux/amd64 alone: nothing is emulated and nothing pretends to be arm64. The
Dart tag will stay native on both architectures.
The Flutter flavour starts FROM dart:3.12.2 — exactly the Dart SDK that
Flutter 3.44.9 bundles — and then installs Flutter 3.44.9 from Google's own
release archive, verified against a pinned sha256 (scripts/install-flutter.sh).
PATH puts /opt/flutter/bin first, so dart and flutter in the image are
the same SDK pair.
It is not built on a third-party registry image. The mirror this repo
originally targeted, ghcr.io/cirruslabs/flutter, stopped at 3.44.0:
manifests/3.44.9 answers MANIFEST_UNKNOWN, so a FROM there would fail at
the first instruction. The operator's decision was provenance by digest over a
stale tag — Google's release manifest plus a sha256 is a pin, a mirror that lags
is not.
Once the package is published, a consumer builds FROM it:
FROM ghcr.io/dip-develop/jaspr-build-image:dart-3.13.4-jaspr-0.23.5-tailwind-4.3.0 AS buildjaspr_cli is already activated as jaspr, and tailwindcss is on PATH at
/usr/local/bin/tailwindcss — a consumer must not install either itself.
Every pin is part of the tag, so a bump changes the tag and cache-busts the build automatically. Change all of these together, then run the check:
- The
constblock intool/check_image_build.dart— the only place a pin is chosen:dartPin,flutterPin,flutterDartPin(the Dart SDK the Flutter release bundles),jasprCliPin,tailwindPin, and for a Tailwind release alsotailwindX64AssetId,tailwindArm64AssetId,tailwindX64Sha256andtailwindArm64Sha256; for a Flutter release alsoflutterArchiveSha256. The two tag strings are derived from these. - The workflow
env:block in.github/workflows/build-image.yml(JASPR_CLI_VERSION,TW_VERSION,FLUTTER_VERSION) and the two literaltags:lines in that file. - The two
# image:comments (first line ofdocker/dart.Dockerfileanddocker/flutter.Dockerfile) and the twoFROMlines — the Dart flavour'sdart:<dartPin>, the Flutter flavour'sdart:<flutterDartPin>. - The version table in
scripts/install-tailwind.sh. A new Tailwind version needs a newcasebranch with fresh asset ids and digests for both architectures; editing the existing branch would ship a binary that does not match its own sha256. - For a Flutter bump, the version table in
scripts/install-flutter.sh— add a newcasebranch with the new archive path and the sha256 from the release manifest. If the bundled Dart moves,flutterDartPinin theconstblock and the FlutterFROMline move with it. .github/workflows/ci.yml'ssdk:line, which must stay equal todartPin, andpubspec.yaml'senvironment: sdk:constraint, which must still admit the pinned Dart.dart run tool/check_image_build.dartfrom the repo root — it fails and names the file that still disagrees.- The consumer
FROMlines in the sites listed in the table above, as part of the migration in issue #2.
dart run tool/check_image_build.dartRun it from the repo root. It is pin hygiene, not a release gate: it reports drift and exits non-zero so CI shows it, with nothing measured against a threshold.
The single-source rule, from the checker's doc comment:
The
constblock intool/check_image_build.dartis the only place a pin is chosen. Every copy of a pin that the checker owns is asserted byte-for-byte: the Dockerfile# image:comment, theFROMline, the workflowenv:block, the workflowtags:line, the installer version tables and theci.ymlsdk:line.README.mdalso spells the pins out, but it is documentation, not an assertion target.
It also asserts the structure that makes those copies safe: the two Dockerfiles
are byte-identical from their # ---- end of flavour-specific setup ---- marker
down (a file-vs-file equality, so deleting the same line from both tails would
pass — a known, accepted limit, since the per-pin assertions are what catch a
pin); build-image.yml has exactly the jobs dart-image and flutter-image in
that order, each publishing its own expected platforms
(linux/amd64,linux/arm64 for the Dart tag, linux/amd64 for the Flutter tag —
read from that job's docker/build-push-action step, so a platforms: input on
another step such as setup-qemu-action is not mistaken for it), building its
own file:, with push: true, provenance: false, its own GHCR
login and its own build args; its permissions: block grants only
contents: read and packages: write; its only secret is GITHUB_TOKEN; it
publishes only on push: main; and ci.yml stays read-only with one job that
triggers on pull_request.
-
Merge the PR. The merge itself triggers the publish:
build-image.ymlruns onpush: main, filtered todocker/**,scripts/**and the workflow file. -
Watch the run finish on the Actions tab or with
gh run list --workflow build-image.yml. -
Set the package visibility to PUBLIC: Settings → Packages →
jaspr-build-image→ Change visibility. Both cross-org consumers will pull anonymously, so a private package breaks them. -
Verify each tag anonymously, reading the raw manifest — the two tags do not have the same shape:
docker logout ghcr.io docker buildx imagetools inspect --raw \ ghcr.io/dip-develop/jaspr-build-image:<tag> | jq -r .mediaType
-
Dart tag →
application/vnd.oci.image.index.v1+json, and its.manifestsarray has exactly two entries:docker buildx imagetools inspect --raw \ ghcr.io/dip-develop/jaspr-build-image:dart-3.13.4-jaspr-0.23.5-tailwind-4.3.0 \ | jq -r '.manifests[].platform | "\(.os)/\(.architecture)"'
-
Flutter tag →
application/vnd.oci.image.manifest.v1+jsonwith no.manifestsat all: one platform plusprovenance: falseis pushed as a plain OCI manifest, not an index. Its one platform comes from the image config:docker buildx imagetools inspect \ --format '{{.Image.OS}}/{{.Image.Architecture}}' \ ghcr.io/dip-develop/jaspr-build-image:flutter-3.44.9-jaspr-0.23.5-tailwind-4.3.0That must print
linux/amd64. A plainimagetools inspecton this tag prints only Name, MediaType and Digest — no platform list — and that is correct, not a failure. A second platform here would be a lie.
Logging out first is the point:
imagetools inspectuses the caller's stored credentials, so it succeeds on a package that is still private and the check would prove nothing. The credential-free equivalent is the anonymous-token probe —curl -fsSL "https://ghcr.io/token?scope=repository:dip-develop/jaspr-build-image:pull"only issues a token for a public package, and the manifest request made with it is what a consumer does. -
-
If the Tailwind fetch 504s, retry with
workflow_dispatch(gh workflow run build-image.yml) — but only if nothing was pushed. The two jobs are independent:dart-imagecan finish and publish its tag whileflutter-imagefails, so a re-run after a partial success rewrites a tag that already exists rather than starting from an empty package.
api.github.com content endpoints (the asset API this repo downloads from) also
return 504 intermittently, so even the one-time download can fail. The fallback
is to vendor the two ~119 MB binaries in this repo and COPY plus
sha256sum -c them instead of curling. That is a deliberate owner decision
(repo bloat), not a drive-by change — do not add vendored binaries without it.
All three pinned downloads were verified against the real bytes on 2026-11-08:
the two Tailwind binaries, and the Flutter 3.44.9 Linux archive (streamed
through sha256sum and compared with the official release manifest), so the pins
themselves are known-good; a failure at build time is the network, not the data.
Flutter publishes no linux/arm64 Linux archive; the consequence for this
repo's tags is spelled out in Why the Flutter tag is amd64-only.
docker/— the two Dockerfiles; flavour-specific above their# ---- end of flavour-specific setup ----marker, byte-identical from it down by design.scripts/—install-tailwind.shandinstall-flutter.sh, the only holders of the Tailwind asset ids and digests and of the Flutter archive path and digest.tool/—check_image_build.dart, the pin checker..github/workflows/—build-image.ymlpublishes;ci.ymlruns the checker read-only.
MIT — see LICENSE. The Dockerfiles, the scripts, the checker and the workflow in this repository are covered by it.
The published images bundle third-party SDKs that keep their own licences: the Dart SDK and the Flutter SDK are BSD-3-Clause, and the Tailwind CSS standalone CLI is MIT. This repository's MIT grant covers its own files; it does not relicense those bundled SDKs.