Install (Hetzner)
Provisioning checklist for spinning up the Fabric cloud control plane on Hetzner. Target: under 10 minutes from "click Buy" to "/healthz returns 200." Source-of-truth: ~/.openclaw/workspace/optimalOS/infra/hetzner/PROVISIONING-CHECKLIST.md.
This is the path Carlos walked on 2026-05-03/04 to bring https://fabric.optimal.miami live. It is also the path another operator would follow to host their own Fabric instance.
What you get
- A CX22 or CX32 Debian 12 box in a Hetzner region you choose (Carlos uses Ashburn, US-East).
- A Cloudflare-Tunnel-fronted hostname (no public ingress), with the tunnel running as a systemd unit on the box.
optimalos.servicerunning the cloud-mode bundle on:3000.- An invite-gated single-tenant install ready for first-passphrase setup.
The Pi (or laptop, or future Mac mini) joins as a paired device after the box is up; see Pairing.
Pre-flight
- Hetzner Cloud account with a payment method (about a minute if first time).
- Cloudflare account with a zone you control (Carlos uses
optimal.miami). - An
~/.ssh/id_ed25519.pubto paste into the Hetzner SSH-key form. Password auth is disabled by the bootstrap. - A Tailscale auth key, or willingness to interactive-auth from the box.
- A Supabase project URL plus service-role key. Carlos uses the existing
hbfalrpswysryltysonminstance shared withoptimal.miami; managed-Supabase v1 per Decision-ledger #7.
Step 1. Buy the box (about 2 minutes)
https://console.hetzner.cloud/then New Project namedoptimalos-fabric(or your preferred slug).- Add Server: pick a US-East region, Debian 12, CX22 (€3.79/mo, 2 vCPU, 4 GB RAM) or CX32 (€5.83/mo, 2 vCPU, 7.6 GB RAM). Carlos was upgraded to CX32 mid-deploy; CX22 is sufficient for v1.
- Paste your SSH key, name the box
fabric-prod-1, and click Create. - Note the public IPv4 address.
Step 2. Bootstrap (about 4 minutes)
ssh root@<ipv4> # accept host key
curl -fsSL <repo>/infra/hetzner/cloud-init.sh | sudo bashThe bootstrap script (committed at infra/hetzner/cloud-init.sh) sets up:
- a non-root
optimaluser bunruntimecaddy(installed but not used for the tunnel path; it stays available if you want a second hostname)cloudflaredpackageufwallowing port 22 from any source initially (locked down to Tailscale after Step 3)/opt/optimalos/{app,workflows,operations,data}directories- a stub
optimalos.serviceunit at/etc/systemd/system/optimalos.service
Wait for the "bootstrap complete" banner, then verify no errors:
cat /var/log/optimalos-bootstrap.log | tail -40Step 3. Tailscale join (about 1 minute)
sudo tailscale up # visit URL, approve on your tailnet
tailscale ip -4 # note the 100.x address
ssh root@<tailscale-ip> # confirm SSH works over Tailscale from your laptopOnce Tailscale SSH works, you can disable the public IPv4 in the Hetzner console (optional). The bootstrap UFW rules already restrict port 22 to the Tailscale interface; the public IP can stay if you want a fallback.
Carlos's instance left public 22 open for the first 24 hours and has Tailscale lock-down queued in the kanban as a Phase 15 prep task.
Step 4. Cloudflare Tunnel (about 2 minutes)
sudo cloudflared tunnel login # paste URL into laptop browser, auth
sudo cloudflared tunnel create fabric-prod # note the UUID
sudo cp /root/.cloudflared/<uuid>.json /etc/cloudflared/<uuid>.json
sudo nano /etc/cloudflared/config.yml # replace both [TBD] UUID slots
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared # activeThe shipped config.yml template ingresses fabric.optimal.miami to http://localhost:3000 with a 404 fallback. Adjust the hostname for your zone.
In the Cloudflare dashboard: DNS then add CNAME fabric.<your-zone> to <uuid>.cfargotunnel.com, proxy ON (orange cloud).
Step 5. Secrets and start (about 1 minute)
sudo cp /opt/optimalos/secrets.env.example /opt/optimalos/secrets.env
# Generate a JWT signing key (decoded length must be at least 32 bytes for HS256):
openssl rand -base64 32 # paste into JWT_SIGNING_KEY=
# Pick an INVITE_PASSWORD (single-tenant gate per Decision-ledger #20a).
# Make it memorable but unguessable; the server logs a loud warning if blank.
sudo nano /opt/optimalos/secrets.env # fill remaining [TBD] (Supabase URL + service key)
sudo chmod 600 /opt/optimalos/secrets.env
sudo chown optimal:optimal /opt/optimalos/secrets.envBuild and ship the Bun bundle (from your laptop, against your optimalOS checkout):
cd ~/path/to/optimalOS
pnpm build:cloud
rsync -avz --delete -e "ssh -i ~/.ssh/id_ed25519" \
dist/ root@<ip>:/opt/optimalos/app/
ssh root@<ip> "
chown -R optimal:optimal /opt/optimalos/app && \
find /opt/optimalos/app -type f -exec chmod 644 {} \; && \
find /opt/optimalos/app -type d -exec chmod 755 {} \;"Start it:
sudo systemctl start optimalos
sudo systemctl status optimalos # active (running)Step 6. Apply vault migrations
The Bun server runs at /healthz immediately, but vault routes will 500 until the Supabase tables exist. Three idempotent migrations (mirrored at ~/.optimalos/transfers/2026050*.sql):
| File | Adds |
|---|---|
20260503215150_fabric_vault_phase_10a_2.sql | vault_entries, vault_recipients, vault_access_log |
20260503221013_fabric_devices_phase_10b_3.sql | devices, pairing_tokens, FK on vault_recipients.device_id |
20260503235721_fabric_vault_canary_phase_10a_7.sql | canary_ciphertext column on vault_recipients |
Two paths; pick whichever works (migrations are idempotent so re-runs are safe). Full procedure including the schema_migrations stamp lives in Runbook.
Step 7. Verify (about 30 seconds)
curl -s -o /dev/null -w "healthz HTTP %{http_code}\n" \
https://fabric.<your-zone>/healthz
curl -s -o /dev/null -w "vault/setup HTTP %{http_code}\n" \
https://fabric.<your-zone>/vault/setup
curl -s https://fabric.<your-zone>/api/version | head -c 200; echo
dig +short fabric.<your-zone> CNAMEExpected: 200, 200, a JSON version blob, and the cfargotunnel CNAME. If anything is wrong, see Runbook then Common breakages.
Step 8. First passphrase ceremony
In any modern browser (iPad Safari is the canonical mobile target):
- Open
https://fabric.<your-zone>/ - Sign in with
INVITE_PASSWORDfrom/opt/optimalos/secrets.env - Navigate to
/vault/setup - Walk the four-step wizard: passphrase, passkey, recovery phrase, challenge
Total time: about 3 seconds for crypto plus however long you spend writing the recovery phrase down. Two vault_recipients rows (browser plus recovery) appear when you finish.
A full walkthrough including what each step does and where to look when it breaks is in Quickstart.
Rollback and kill switch
If anything is wrong and you want to take the box out of service:
Soft rollback (stop services, keep box):
sudo systemctl stop optimalos cloudflared caddy
sudo systemctl disable optimalos cloudflared caddy
# Cloudflare dashboard: delete the fabric.<your-zone> CNAMEHard rollback (delete the Hetzner box entirely):
- Hetzner console then
fabric-prod-1then Delete. - Cloudflare dashboard then Tunnels then
fabric-prodthen Delete tunnel (also removes the cfargotunnel record). - Cloudflare DNS: delete the
fabric.<your-zone>CNAME.
Hetzner billing settles within minutes (hourly billing).
Repoint to Pi (emergency restore):
CF DNS: optimal.miami CNAME → a6f4487b-dd02-4702-9b8c-2ccea348b2a7.cfargotunnel.comThat UUID is the Pi tunnel from /etc/cloudflared/config.yml on the Pi. Carlos uses this if Hetzner needs to come down for any reason and the Pi has to absorb traffic.
After install
- Pair your first device with Pairing.
- Add your first credential per Quickstart then Add your first credential.
- Lock down public 22 on UFW once Tailscale is verified working.
- Backup
/opt/optimalos/secrets.envto your password manager. If the box is destroyed andJWT_SIGNING_KEYis lost, every browser session is invalidated (not catastrophic, but worth a note).
Source
- Provisioning checklist:
~/.openclaw/workspace/optimalOS/infra/hetzner/PROVISIONING-CHECKLIST.md - Bootstrap script:
~/.openclaw/workspace/optimalOS/infra/hetzner/cloud-init.sh - Caddyfile and
cloudflared/config.ymltemplates:~/.openclaw/workspace/optimalOS/infra/hetzner/ - Hetzner handoff (load-bearing on session start):
~/.optimalos/transfers/fabric-hetzner-handoff-2026-05-04.md - Bootstrap zip (8 files for hands-off provisioning):
~/.optimalos/transfers/fabric-hetzner-bootstrap.zip