Files
nucleic/containers/nucleic-runner

nucleic-runner container image

The Covalence runner's host-of-record container (design: docs/COVALENCE_RUNNER.md §2, §9; narOS milestone N3: docs/NAROS.md §7.2). Extends the narOS agent tier (ghcr.io/abkslm/naros-agent, built from os/ — it replaced nucleic-sandbox) with the nucleicd daemon, so a single container is both the sync host of record and the agent sandbox — the CLOUD_RUNTIME RunSpec.container = nil model, where the container is the sandbox. The base brings nash forced as /bin/sh (docs/NASH.md §7.3), the narOS identity/manifest, and the full agent toolchain; this image stamps the runner-tier VARIANT on top.

Building

The nucleicd Linux build is real — the CLOUD_RUNTIME Phase-2 port landed: the full NucleicProtocol test suite (Noise handshake vectors, relay-enrollment PoP, sealed-credential crypto) passes on Linux under swift-crypto, GRDB runs on Linux, the approval server listens on BSD sockets, secrets live in the 0600-file store, and the relay WebSocket is SwiftNIO. A booted Linux nucleicd enrolls with the deployed relay and mints pairing codes — verified on both aarch64 (glibc) and x86_64 (the fully-static musl build, run under qemu).

  • Self-host on a Mac: you don't need this image at all — swift run nucleicd runs the daemon natively, joining the mesh as a PeerKind.cloud host.
  • Container build — Cloudflare Containers run linux/amd64 ONLY. The image binary is a fully-static x86_64-musl cross-build (works at native speed from any host arch; the script installs the Static Linux SDK and gives its sysroot the SQLite that GRDB links):
    docker run --rm -v "$PWD":/src -w /src swift:6.3-bookworm scripts/build-nucleicd-linux.sh
    cp .build/x86_64-swift-linux-musl/release/nucleicd containers/nucleic-runner/
    docker build --platform linux/amd64 \
      -t registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1 containers/nucleic-runner
    wrangler containers push registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1
    

Self-hosted variant (tailscale)

The image ships in two flavours, chosen by the WITH_TAILSCALE build arg:

  • Nucleic-hosted (default, WITH_TAILSCALE=0) — what CI builds and pushes to Cloudflare. These runners are reachable only over Nucleic's relay infra (nucleicd listens relay-only), so no tailscale is installed; it would be dead weight the cloud runner never uses.
  • Self-hosted (WITH_TAILSCALE=1) — for an operator who wants the container reachable over their own tailnet. This installs the OS-level tailscale package. Build + tag it separately:
    docker build --platform linux/amd64 --build-arg WITH_TAILSCALE=1 \
      -t nucleic-runner:self-hosted containers/nucleic-runner
    
    nucleicd's own Tailnet transport (TailscaleKit) is Darwin-only with no Linux build, so on Linux "tailscale" means the system daemon: the operator supplies a TS_AUTHKEY and runs tailscaled + tailscale up alongside nucleicd (nothing starts it automatically — the default entrypoint is just the daemon). A Mac self-host needs no container at all — see Building above.

Registry — Cloudflare's managed registry (not GHCR)

The image the containers block deploys lives in Cloudflare's own managed registry (registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner), not GHCR: Cloudflare Containers / Wrangler don't support GHCR as a pull source, and any external registry needs an extra wrangler containers registries configure + pull credentials. The managed registry needs none of that — wrangler containers push authenticates with the account's CLOUDFLARE_API_TOKEN and Cloudflare pulls it at deploy time with no further config. .github/workflows/runner-image.yml does the build + push in CI (below); the base naros-agent image stays on GHCR because it's only pulled at build time (by Docker in CI), never by Cloudflare. The FROM tag is pinned by the Dockerfile's NAROS_VERSION arg, whose default must match os/VERSION (CI asserts the lockstep) — and the tag exists only after .github/workflows/naros.yml has published that release, so sequence a version bump as: naros.yml green first, then this image.

One gate before this image deploys to Cloudflare: runner-image.yml must run once (on a main/dev push or via workflow_dispatch) to publish registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1 — the containers block in cloud/nucleic-runner/wrangler.jsonc references it. The rest of the runner stack (Worker + pool DO) deploys and is tested without it.

Runtime contract

The pool (RunnerPool DO → RunnerContainer DO) injects at start: NUCLEIC_RUNNER_{POOL_ID,INSTANCE_ID,EPOCH,INSTANCE_TOKEN,CONTROL_URL} and optional NUCLEIC_RELAY_URL. nucleicd enrolls with the relay, listens relay-only, heartbeats the pool with its fencing epoch, and serves a one-shot pairing payload on loopback :9200 for the provisioning proxy.