Superseded VM runbook. The Hetzner + Tailscale target below is no longer current; Fly is the current cloud-runner target. Do not provision the current target using M1 or
bootstrap-host.sh. The historical Bucket-A daemon scaffolding is retained below. Bucket-C jobs usedeploy/fly-runner/, not this directory's full-snapshot secret materialization. See the current cloud guide for the implemented runner boundary and the separate activation prerequisites.
Task #5 of plan ent_aff87747b49e338790568af6. Design rationale + the bucket
classification (which daemon moves vs stays device-side) live in
docs/cloud_hosting.md. This directory is the
executable side of that design.
| Decision | Choice |
|---|---|
| Host | Hetzner Cloud, EU (Falkenstein/Nuremberg), CAX11 (2 vCPU ARM, 4 GB, ~€4/mo) |
| Topology | Containers on one small VM (Option 2 image + Option 1 host); code-touching T4 work → GitHub Actions (Bucket C) |
| Network | Tailscale (private mesh; no public SSH/ports) |
| age key | Per-host key (host compromise is contained + revocable without rotating the primary key) |
claude auth |
ANTHROPIC_API_KEY materialized from the SOPS+age snapshot |
Google (gws) |
B-thin first — Monedula/Sylvia/Cotinga stay device-side until a Google service account (A-svc) is provisioned |
| File | Purpose |
|---|---|
Dockerfile |
Multi-arch Bucket-A daemon image (Python + sops + Claude CLI + repo). Secrets never baked in. |
entrypoint.sh |
Materializes secrets (SOPS+age) → ~/.config/neotoma/.env, then execs the daemon. |
docker-compose.yml |
One container per Bucket-A daemon, shared image. APIS_DRY_RUN=1 by default. |
bootstrap-host.sh |
Idempotent VM bootstrap: Docker, Tailscale, repos, age key, build+up. |
- Create the Hetzner
CAX11(Ubuntu 24.04 arm64), EU region. - From your machine:
tailscale upis done by the bootstrap; generate a Tailscale auth key in the admin console first. - SSH in (over Tailscale once joined) and run:
(clone this repo first, or
ATELES_PRIVATE_REMOTE=<read-only clone URL for ateles-private> \ TS_AUTHKEY=tskey-... \ bash /opt/ateles/ateles/deploy/cloud/bootstrap-host.sh
curlthe script.) It pauses with instructions at any step that needs a secret you haven't supplied.
On the host:
mkdir -p /opt/ateles/age && age-keygen -o /opt/ateles/age/keys.txt && chmod 600 /opt/ateles/age/keys.txt
grep '^# public key:' /opt/ateles/age/keys.txt # copy the age1... valueThen, in ateles-private: add that age1... as a recipient in .sops.yaml,
re-encrypt the snapshot (execution/scripts/secrets_publish.py), commit, and
git -C /opt/ateles/ateles-private pull on the host. Confirm
ANTHROPIC_API_KEY and NEOTOMA_BEARER_TOKEN_PROD are in the published
snapshot (the latter is auto-promoted for the remote NEOTOMA_BASE_URL).
cd /opt/ateles/ateles/deploy/cloud && docker compose up -d
docker compose logs -f apis # expect: secrets materialized, SSE subscribe,
# "[watchdog] starting", clean sweepsWith APIS_DRY_RUN=1 nothing dispatches — you're confirming the container
materializes secrets, authenticates to prod Neotoma (no 401s), and connects
SSE.
For each daemon: confirm it's healthy in the cloud, then stop its Studio copy
(launchctl bootout gui/$(id -u)/com.ateles.<name>), then set
APIS_DRY_RUN=0 (for Apis) / remove dry-run and docker compose up -d <svc>.
Single-consumer rule: never run the same daemon on the Studio and the cloud
— they double-consume SSE and double-act. Rollback = stop the cloud service,
re-enable the Studio plist.
Redirect Cicada/Vanellus dispatch to GitHub Actions ephemeral runners (Loxia is already there). Tracked separately; not required to get Bucket A off the Studio.
Rebuild the image (it carries the code): docker compose build && docker compose up -d.
Optionally add a systemd timer that git pulls + rebuilds on origin/main
(the container analog of rc-autodeploy).
- Secrets: the host is just another materialize target for the existing
SOPS+age snapshot — no new distribution mechanism. The age key + the
ateles-privateclone are mounted read-only; plaintext exists only in the running container's env. - Device-bound daemons (Tyto, Strix, Cyphorhinus, computer-use, the
gwsbridge) stay as launchd agents on the operator's device(s) — Bucket B.