Self-hosted Tangled spindle

S0: Build the spindle mill image from Tangled v1.20.0 (ENG-463) #2

Merged demi.dev
targeting main from stoopkidddd/eng-463-s0-build-the-spindle-mill-image-from-tangled-v1200
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-463-s0-build-the-spindle-mill-image-from-tangled-v1200

Linear: ENG-463

Summary #

A container image that runs the spindle in the mill role. Railway runs it in ENG-465.

  • mill/Dockerfile: a multi-stage linux/amd64 build.
    • The build stage is nixos/nix:2.35.4, the same tag as the checks. It builds git+https://tangled.org/tangled.org/core?ref=refs/tags/v1.20.0#pkgsStatic-spindle. The static build succeeded, so the closure fallback wasn't needed.
    • The runtime stage is digest-pinned debian:forky-slim, with git (2.53.0) and ca-certificates.
    • It runs as UID/GID 10001, exposes 6555, and puts state under /data.
  • mill/entrypoint.sh:
    • writes SPINDLE_SIGNING_KEY_PEM to /run/spindle/key.pem (mode 600) and unsets the variable;
    • sets SPINDLE_SERVER_KEY_PATH, then runs exec spindle;
    • exits 1 with a clear message if the variable is missing.
  • mise.toml: adds the mill:build and mill:smoke tasks.
  • scripts/mill-smoke.sh: generates a throwaway P-256 key in a temp dir, runs the image as a dev mill with a disk artifact store, checks health, the exact DID key, key file mode and SIGTERM, then cleans up.

Verify (lead, on the branch rebased onto main) #

$ mise run check            → check_exit=0 (includes the executor flake)
$ mise run mill:build       → build_exit=0 (cold build 43 min under Rosetta; image 264 MiB)
$ mise run mill:smoke
GET /_health: 200
GET /.well-known/did.json: did:web:localhost, matching P-256 public key
Signing key permissions: 600
SIGTERM: clean exit (0)
Private-key markers in container output: 0
mill: smoke passed

BEGIN EC PRIVATE KEY appears 0 times in the captured build and smoke logs. No smoke containers or volumes are left behind.

Deviations #

  • Runtime base is Debian testing (forky-slim), digest-pinned. Tangled requires git ≥ 2.49, and trixie ships 2.47.3.
  • filter-syscalls = false in the Nix build stage only. Rosetta rejects Nix's seccomp filter. The runtime image is unaffected.
  • The smoke test uses SPINDLE_CONTACT_EMAIL=smoke@example.invalid, because the mill role requires a contact email.
  • Round 2 removed an out-of-scope README section. Its facts moved to ENG-465.

🤖 Generated with Claude Code

Commits 3

S0: Fix mill ownership on root-owned volumes (ENG-463)
S0: Remove out-of-scope mill README section (ENG-463)
S0: Build the mill image from Tangled v1.20.0 (ENG-463)