SailEver · infra/oci · status 2026-10-23
The new server runs on three Oracle Cloud VMs in Amsterdam (eu-amsterdam-1, compartment sailever), built with OpenTofu from infra/oci/. The app VM sailever-beta (public subnet, only ports 80 and 443 open) runs kamal-proxy with Let's Encrypt and the Rails container for beta.sailever.com (website: sign-in, logbook, pairing page) and api.sailever.com (watch API). The DB VM sailever-db sits in a private subnet without a public IP: Postgres 16 with its data on a 50 GB block volume, reachable only from the app servers on 5432, over TLS that Rails checks (sslmode=verify-full against a private CA made by tofu). Uploaded files are in the OCI Object Storage bucket sailever-files; a nightly encrypted pg_dump goes to sailever-backups; mail goes out through OCI Email Delivery. The worker VM sailever-worker (private subnet, no inbound port, 2 OCPU / 12 GB) runs all background jobs: the Rails jobs role (Solid Queue, uploads first; the app VM keeps one fallback thread for them) and, at the lowest priority, the tile builds (depth layer and monthly basemap), which upload to R2. The move of the Rails jobs and the basemap build to the worker VM is in code, not deployed yet (ADR 011). Map tiles (with the depth layer) and the email logo come from Cloudflare R2, served on tiles.sailever.com (a Worker) and assets.sailever.com. SSH reaches each VM only through its own Cloudflare Tunnel behind Cloudflare Access (ssh / ssh-db / ssh-worker.sailever.com), for the owner (One-time PIN) and the deploy service token. www.sailever.com stays on the old Hetzner server until launch.
Status 2026-10-23: live since the first deploy on 2026-10-17; the worker VM since 2026-10-22, building the depth layer v2; WatchApp 1.1.0 on Garmin and Wear OS pairs and uploads from real watches (closed beta: only invited accounts can sign in). Two kinds of change: the app is deployed by GitHub Actions (Run workflow on main); infrastructure changes only by tofu on the owner's Mac.
Pick a view below. Every box is a button: click it (or Tab to it and press Enter) to see what it is, where it lives, which file defines it, its status and who can reach it. Related items in the panel jump to their box, in another view if needed.
Still open in docs/plans/beta_server_next_steps.md:
sailever.com/pair reaches beta through the apex 301 to www and the old app's 302; the planned Cloudflare 302 rule is not active. At launch the new app on www serves /pair itself.tofu apply (NSG rule db_postgres_worker), infra/oci/db-allow-worker.sh once on the DB VM, then the server deploy (role jobs) and the Worker deploy (basemap build).www moves to the new server, SAILEVER_BETA=false, beta → www 301, both /pair redirects to www, old app archived; then the beta applications go.docs/credentials.md §8). Later (3.11): load balancer, second app server.Request paths. Web and API traffic resolves through Cloudflare DNS but goes straight to the app VM's public IP (DNS only, not proxied), where kamal-proxy holds the Let's Encrypt certificates. Map tiles and the email logo never touch the VMs: browsers fetch tiles from the Worker on tiles.sailever.com, mail apps load the logo from assets.sailever.com, both backed by Cloudflare R2. The DB VM has no public IP: only the app servers and the worker VM reach Postgres on 5432, over TLS. The worker VM takes no connection at all; it runs every background job (in code, not deployed yet: the Rails jobs role, uploads first, and the basemap build; live: the depth build, at the lowest priority), and its jobs only reach out: to Postgres inside the VCN, everything else through the NAT gateway. SSH never touches a public IP: each VM opens its own tunnel to Cloudflare from the inside (the private VMs through the NAT gateway), behind Cloudflare Access. Port 22 is closed everywhere.
Who can do what. The password manager is the source of truth (docs/credentials.md); the Mac holds short-lived copies for tofu, GitHub Actions hold copies as repository secrets. All three VMs take the same deploy key, each behind its own tunnel and Access application with the same two policies. The Rails app acts as its own OCI user sailever-app with two keys; the tile builds on the worker VM write to R2 with a token limited to one bucket, which only the Worker deploy hands to them. No secret values appear on this page.
Where data lives and how it is copied. Postgres runs on the DB VM with its data on the 50 GB data volume, which survives a VM replacement. Uploaded files are in the Object Storage bucket sailever-files, so the app VM keeps no data. Two independent copies of the database: weekly volume backups inside OCI, and a nightly age-encrypted pg_dump that a Rails job writes to sailever-backups (write-only; in code it runs in the jobs role on the worker VM, today still in Puma on the app VM); the owner restores them on the Mac. The tofu state is a separate, encrypted object; it also holds the Postgres TLS keys. The R2 buckets on Cloudflare hold only rebuildable or git-tracked files and are in no backup.
How a change travels. Every commit and push passes mise guard locally; GitHub re-runs the checks in CI but can't block (GitHub Free). The app deploys only when the owner presses Run workflow on main: Kamal goes through the Access applications, the web role to the app VM, the jobs role to the worker VM, the Postgres accessory to the DB VM. The map tiles are built on the worker VM and uploaded to R2, the depth layer every 30 days and the basemap monthly, in a low-priority container the Worker workflow deploys; tiles.yml only runs the Worker's tests. The jobs role and the basemap build are in code, not deployed yet. The documentation is built by Cloudflare Pages on every push to main and shown only to the owner, behind Access. The email logo is uploaded by hand with wrangler. Infrastructure, including the Worker code, changes only by tofu on the owner's Mac.
Wide diagrams scroll sideways on small screens. Arrow styles follow the status of their ends: solid = live, dashed blue = created but not in use or in code only, long-dashed grey = later (step 3.11), dotted = old site.
Every box from the four views in one list, grouped by where it lives, with its status and the main place it is defined. Names open the details.
| Component | Status | Defined in |
|---|---|---|
| Owner and Mac | ||
| Live | infra/oci/settings.tf:14 | |
| Live | docs/credentials.md | |
| Live | infra/oci/versions.tf:60-65 | |
| Live | infra/oci/retry-apply.sh:1-40 | |
| Live | infra/oci/README.md:86-93 | |
| Live | infra/oci/versions.tf:73-77 | |
| Live | tools/guard/guard.py:107 | |
| Live | tools/guard/guard.py:189-200 | |
| Live | infra/assets/README.md:15-23 | |
| Live | apps/server/bin/backup-restore-test | |
| Live | infra/oci/versions.tf:62 | |
| Live | infra/oci/versions.tf:23-39 | |
| Live | apps/server/config/deploy.yml:63-64 | |
| Cloudflare: DNS, tunnels, Access | ||
| Live | Made by hand (plan step 2.3, 2.5) | |
| Live | infra/oci/cloudflare.tf:5-15 | |
| Old site, until launch | Made by hand (plan step 2.3) | |
| Live | Made by hand in the dashboard (plan step 2.5, 5.2) | |
| Live | Made by hand (plan step 2.3) | |
| Live | infra/oci/storage.tf:93-101 | |
| Live | infra/oci/cloudflare.tf:51-59 | |
| Live | infra/oci/cloudflare.tf:149-157 | |
| Live | infra/oci/cloudflare.tf:18-20 | |
| Live | infra/oci/cloudflare.tf:116-118 | |
| Live | infra/oci/cloudflare.tf:100-112 | |
| Live | infra/oci/cloudflare.tf:159-171 | |
| Live | infra/oci/cloudflare.tf:75-82 | |
| Live | infra/oci/cloudflare.tf:84-98 | |
| Live | Made by hand (plan step 2.6) | |
| Live | Made by hand (plan step 2.6) | |
| Live | docs/credentials.md §2 | |
| Live | tools/docs/README.md | |
| Live | tools/docs/README.md | |
| Cloudflare: R2 and Workers | ||
| Live | infra/tiles/worker.js:1-13 | |
| Live | infra/oci/tiles.tf:6-10 | |
| Live | infra/oci/assets.tf:5-9 | |
| Live | docs/credentials.md §3 | |
| Oracle Cloud: accounts and network | ||
| Live | Made by hand (plan step 2.1, 2.2) | |
| Live | Made by hand (plan step 2.2) | |
| Live | Made by hand (plan step 2.2) | |
| Live | Made by hand (plan step 2.2) | |
| Live | Made by hand (plan step 2.2, 3.9) | |
| Live | infra/oci/storage.tf:11-18 | |
| Live | infra/oci/iam.tf:1-4 | |
| Live | infra/oci/iam.tf:23-37 | |
| Live | docs/credentials.md §3 | |
| Live | docs/credentials.md §3 | |
| Live | infra/oci/network.tf:27-32 | |
| Live | infra/oci/network.tf:34-39 | |
| Live | infra/oci/network.tf:53-73 | |
| Live | infra/oci/network.tf:75-83 | |
| Live | infra/oci/network.tf:150-159 | |
| Live | infra/oci/network.tf:86-90 | |
| Live | infra/oci/network.tf:92-108 | |
| Live | infra/oci/network.tf:162-166 | |
| Live | infra/oci/network.tf:188-193 | |
| Oracle Cloud: VMs, storage, mail | ||
| Live | infra/oci/vm.tf:37-88 | |
| Live | infra/oci/vm.tf:92-140 | |
| Live | infra/oci/vm.tf | |
| Live | infra/oci/vm.tf:198-207 | |
| Live | infra/oci/vm.tf:209-214 | |
| Live | infra/oci/vm.tf:218-231 | |
| Live | infra/oci/storage.tf:2-8 | |
| Live | infra/oci/storage.tf:40-50 | |
| Live | infra/oci/bootstrap/main.tf:18-28 | |
| Live | infra/oci/versions.tf:26-39 | |
| Live | infra/oci/tls.tf:10-50 | |
| Live | infra/oci/storage.tf:71-74 | |
| On the app VM | ||
| Live | infra/oci/cloud-init.yaml.tftpl:56-74 | |
| Live | infra/oci/cloud-init.yaml.tftpl:22-23 | |
| Live | infra/oci/cloud-init.yaml.tftpl:147-152 | |
| Live | infra/oci/cloud-init.yaml.tftpl:143-146 | |
| Live | apps/server/config/deploy.yml:15-22 | |
| Live | apps/server/config/deploy.yml:5-8 | |
| Live | apps/server/config/recurring.yml:22-25 | |
| On the DB VM | ||
| Live | infra/oci/cloud-init.yaml.tftpl:75-86 | |
| Live | infra/oci/cloud-init.yaml.tftpl:22-23 | |
| Live | infra/oci/cloud-init.yaml.tftpl:147-152 | |
| Live | infra/oci/cloud-init.yaml.tftpl:100-105 | |
| Live | infra/oci/cloud-init.yaml.tftpl:106-127 | |
| Live | infra/oci/cloud-init.yaml.tftpl:86-99 | |
| Live | apps/server/config/deploy.yml:85-115 | |
| On the worker VM | ||
| In code, not applied yet | apps/server/config/deploy.yml:14-26 | |
| Live | infra/worker/config/deploy.yml:36-50 | |
| In code, not applied yet | infra/basemap/README.md | |
| On every VM | ||
| Live | infra/oci/cloud-init.yaml.tftpl:10-20 | |
| Live | infra/oci/cloud-init.yaml.tftpl:25-30 | |
| Later: load balancer (step 3.11) | ||
| Later: step 3.11 | docs/plans/beta_server_next_steps.md 3.11 | |
| Later: step 3.11 | docs/plans/beta_server_next_steps.md 3.11 | |
| Later: step 3.11 | infra/oci/README.md:114 | |
| GitHub | ||
| Live | docs/plans/monorepo_plan.md §6.4 | |
| Live | Set by hand (plan step 1.2) | |
| Live | .github/workflows/guard.yml | |
| Live | .github/workflows/server.yml:26-35 | |
| Live | .github/workflows/android.yml | |
| Live | .github/workflows/tracksim.yml | |
| Live | .github/workflows/tiles.yml:1-4 | |
| Live | .github/workflows/worker.yml:1-8 | |
| Live | .github/dependabot.yml | |
| Live | .github/workflows/server.yml:69-168 | |
| Live | docs/credentials.md §3 | |
| Live | apps/server/config/deploy.yml:7 | |
| Dropped | docs/plans/beta_server_next_steps.md 1.5 | |
| Dropped | docs/plans/beta_server_next_steps.md 1.6 | |
| Outside | ||
| Live | apps/server/config/deploy.yml:47-49 | |
| Live | apps/server/app/views/layouts/mailer.html.erb:11 | |
| Live | apps/garmin-watchapp/source/Config.mc:16 | |
| Old site, until launch | docs/plans/monorepo_plan.md §7.6 | |
| Live | docs/plans/beta_server_next_steps.md 5.2 | |