- Dockerfile 71.2%
- Shell 28.8%
Containerfile version ARGs had defaults too — a second source that drifted (build.sh 3.44.1 vs Containerfile 3.44.0). Made them bare ARGs (no defaults) so the value must come from build.sh's --build-arg, and pulled ANDROID_CMDLINE_TOOLS into build.sh as well. IMAGE_REVISION/CREATED keep defaults (provenance fallbacks). |
||
|---|---|---|
| flutter-android | ||
| go | ||
| build.sh | ||
| README.md | ||
ci-images
Custom container images for the self-hosted Forgejo Actions runner on the workstation. The runner is a single laptop, so instead of downloading Flutter / the Android SDK / Go on every job, the toolchains are baked into local images and the runner references them by label.
Runner setup itself lives in the Logseq runbook Forgejo CI with Rootless Podman.
Images
| Image | Label | Toolchain | Used by |
|---|---|---|---|
localhost/fiatcode-ci-go |
go |
Go 1.26.3, ripgrep, Node.js | peekseq |
localhost/fiatcode-ci-flutter-android |
flutter-android |
Flutter 3.44.0, Android SDK 36 + NDK 28.2, Java 17, Node.js | gbikudus-app, kuwot-app |
Both are based on ghcr.io/catthehacker/ubuntu:act-22.04 — that base already carries Node.js
and git, which the JS actions (checkout, setup-java, upload-artifact) require inside the
job container. Building up from it avoids the node: not found trap.
Build
./build.sh # both
./build.sh go # just the Go image
./build.sh flutter-android # just the Flutter image (slow)
Images go into the local Podman store (localhost/...). No registry, nothing pushed —
the runner runs on the same machine that builds them.
Versioning & provenance
Each build is tagged twice — a version tag and :latest:
localhost/fiatcode-ci-go:1.26.3 localhost/fiatcode-ci-go:latest
localhost/fiatcode-ci-flutter-android:3.44.0 localhost/fiatcode-ci-flutter-android:latest
The runner config rides :latest (rebuild → restart picks it up). To know exactly what a
given image holds, read its OCI labels — they're stamped from the same versions, so they
can't drift from what's installed:
podman image inspect localhost/fiatcode-ci-flutter-android:latest \
--format '{{range $k,$v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}'
# dev.fiatcode.flutter.version=3.44.0
# dev.fiatcode.android.platform=android-36
# dev.fiatcode.android.buildtools=36.0.0
# dev.fiatcode.android.ndk=28.2.13676358
# org.opencontainers.image.revision=<ci-images git short SHA>
# org.opencontainers.image.created=<build timestamp>
org.opencontainers.image.revision ties the running image back to the exact ci-images
commit that built it — so "what's in :latest" is always answerable from the image alone.
Versions are defined once, in build.sh (passed as --build-arg, used for both the tag
and the labels). The Containerfile ARG defaults are just fallbacks.
Wire into the runner
-
Add the labels in
~/.local/share/forgejo-runner/config.yml, keepingdockeras the generic fallback, and setforce_pull: falseso Podman uses the local images instead of trying to pulllocalhost/...from a registry:runner: labels: - docker:docker://ghcr.io/catthehacker/ubuntu:act-22.04 - go:docker://localhost/fiatcode-ci-go:latest - flutter-android:docker://localhost/fiatcode-ci-flutter-android:latest container: docker_host: "unix:///run/user/1000/podman/podman.sock" force_pull: false -
Restart the runner so it re-advertises labels:
systemctl --user restart forgejo-runner.serviceBuild the images before restarting. A label that points at a missing image means jobs routed to it fail.
Use in a workflow
Switch runs-on to the baked label and delete the now-redundant toolchain-setup steps.
peekseq .forgejo/workflows/ci.yml:
# before
jobs:
test:
runs-on: docker
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/setup-go@v5 # <- drop
- run: sudo apt-get update && sudo apt-get install -y ripgrep # <- drop
- run: go test ./...
# after
jobs:
test:
runs-on: go # Go + ripgrep already in the image
steps:
- uses: actions/checkout@v4
- run: go vet ./...
- run: go test ./...
- run: go build ./...
gbikudus-app / kuwot-app Flutter workflows:
# before
runs-on: docker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4 # <- drop
- uses: https://github.com/android-actions/setup-android@v3 # <- drop
- uses: https://github.com/subosito/flutter-action@v2 # <- drop
- run: flutter pub get
...
# after
runs-on: flutter-android # Flutter + Android SDK + Java already in the image
steps:
- uses: actions/checkout@v4
- run: flutter pub get
...
Everything else (the keystore decode, google-services.json decode in the release job, the
tag-driven build) stays the same.
Maintenance
Baked images go stale when a toolchain bumps. All versions live in build.sh — bump there,
rebuild, restart the runner:
- Flutter → bump
FLUTTER_VERSION(match the workstation'sflutter --version). - Go → bump
GO_VERSION(match the repo'sgo.mod). - Android → if a build re-downloads an SDK platform/NDK every run, the baked
ANDROID_PLATFORM/ANDROID_BUILD_TOOLS/ANDROID_NDKno longer match the app's Flutter version (check Flutter's gradle defaults:compileSdkVersion/ndkVersion) — bump and rebuild.
The version tag + labels update automatically since build.sh drives both.
The docker label stays as a fallback for any repo that doesn't want a baked toolchain.