Docker and Kubernetes
Two hosts that need no OS to own. Both are proven: CI builds the image and runs a real loop in it, and applies the shipped manifest to a real cluster.
The rule the container follows
It clones, then gets out of the way. It never re-implements start.sh.
Unless there is nothing to clone. A share or blob station has no repository, so the image carries the station payload - planted at build time by the same bootstrap.sh that plants a transport repo - and the entrypoint copies it into place instead of cloning. The git path is unchanged and the clone is still authoritative there; the baked payload is a fallback for the transports with no repo, not a second opinion about the ones that have one.
The relay needs one more thing and the image has it: heliograph-seal, the one binary the far side is ever given, built from the same commit as the payload. The entrypoint points a relay station at it.
Which checksum it used is printed, because the two are worth different things. RELAY_SEAL_SHA256 set by you - from SHA256SUMS in the release, where heliograph-seal-linux-amd64 and its arm64 twin are published beside the CLI - compares a binary you did not build against a number you did not choose. The image's own record - offered only when you set none - proves the binary has not changed since the image was built, and nothing about whether the right one was built, since anyone who could replace one could replace both. Both beat the third state, which is the one that existed until now: no binary, and a relay station that refused to start.
entrypoint.sh resolves the repo URL, clones or reuses a checkout, and exec ./start.sh. Everything past the clone is start.sh's alone. That boundary is why the container cannot drift from a station started by hand.
Docker
docker run --rm \
-e REPO_URL=https://github.com/your-org/your-transport-repo.git \
-e GIT_TOKEN_FILE=/run/secrets/token \
-e GIT_TOKEN_USER=x-access-token \
-v /path/to/token:/run/secrets/token:ro \
ghcr.io/dbhq-uk/heliograph-toolkit:1.0.0-rc1
The tag has no v. Git tag v1.0.0-rc1 publishes image tag 1.0.0-rc1. This costs somebody twenty minutes roughly every time.
| variable | |
|---|---|
REPO_URL | the transport repo to clone. May also be the first positional argument |
BRANCH | which branch to check out |
GIT_TOKEN / GIT_TOKEN_FILE | the credential. Prefer the file |
GIT_TOKEN_USER | x-access-token for GitHub, oauth2 for GitLab, empty for Azure DevOps |
HELIOGRAPH_STATUS_PORT | serve a liveness endpoint. Off unless set |
everything station.sh reads | passed straight through |
Arguments after the image name go to start.sh, so -- --once works exactly as it does on a terminal.
The unprivileged user is not a security boundary
The image runs as uid 1000 because the runners refuse to run as root and because it is the right default. It is not isolation: anything the container can reach, the station can reach. The security argument is the account and the gates, not the container.
The status port
HELIOGRAPH_STATUS_PORT starts a tiny status server so a platform that insists on a health check has something to probe. It is off unless set, because ACI, a VM and a plain docker run all need nothing, and an open port nobody asked for is a worse default than a platform-specific setting.
Web App for Containers is the case that forces it: it kills a container with no listening port every 230 seconds.
Kubernetes
kubectl apply -f station/bash/kubernetes/heliograph.yaml
A Deployment with one replica, running as uid 1000, with the transport repo URL and the credential as environment and a mounted secret. Edit the URL and the secret reference; nothing else has to change.
One replica, and it matters. Two stations on one lane both answer the same request, double-run every step and race on delivery. A heliograph log's whole value is that it says what one machine saw.
The manifest is applied to a real kind cluster in CI and driven through a complete run, so the thing published is the thing tested.
Debugging a container that will not start
The failure that wastes the most time is a crash loop, because the logs you need are the ones the platform will not give you while it is restarting.
Reproduce it locally with the same invocation:
docker run --rm -e REPO_URL=... ghcr.io/dbhq-uk/heliograph-toolkit:1.0.0-rc1 -- --check
--check runs the preflight and changes nothing, and it names every blocking problem with a remedy. That is almost always faster than reading platform logs.
On Azure specifically, a VNet-injected ACI reports slowly and gives you nothing while crash-looping - see Azure.