Hetzner cutover runbook — Stratify on app.stratifyinvest.com
Status: COMPLETED (2026-06-03). Stratify is live on Hetzner (128.140.8.187, Docker + Traefik) athttps://app.stratifyinvest.com. Vercel is deactivated. This runbook is retained for reference + rollback; the steps below were executed, not pending.
Moved Stratify from Vercel + Supabase Cloud to coolify-main (128.140.8.187). Did not migrate Loki or Ecosystem Studio (already on Hetzner under other paths).
Pre-flight (Hetzner ready)
- [ ]
stratify-*containers healthy (docker ps | grep stratify) - [ ] Traefik
stratify.yamlinstalled (notstratify-staging.yaml) - [ ] Smoke via IP + Host header:
curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: app.stratifyinvest.com' \
https://128.140.8.187/api/health
curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: app.stratifyinvest.com' \
https://128.140.8.187/supabase/rest/v1/- [ ] Demo login works (after cutover):
[email protected]/ envDEMO_PASSWORD - [ ]
./scripts/hetzner-backup-postgres.shtested once
Isolation verified
| Check | Command | |
|---|---|---|
| No Loki port clash | `ss -lntp \ | grep 5433` on server — only stratify |
| Traefik paths | grep stratify /data/coolify/proxy/dynamic/stratify.yaml — no ecosystem | |
| Compose project | docker compose -p stratify-supabase ps — not supabase (Loki) |
Cloudflare cutover (Toby — requires valid API token)
- Maintenance window — brief 502 acceptable if DNS TTL low.
- Run:
./scripts/hetzner-cloudflare-app-dns.sh
- Sets
app.stratifyinvest.comA →128.140.8.187(proxied default).
- No Zero Trust application covers
app.stratifyinvest.com. The edge gate that
used to sit on the ventures host is gone; /ops and /pilot rely on the app's own auth. Add one if you want it back — docs/runbooks/cloudflare-access.md.
Rollback: Point the app A record back to Vercel IPs / re-enable Vercel DNS.
Post-cutover dashboard updates
| System | Action |
|---|---|
| Stripe | Webhook URL stays https://app.stratifyinvest.com/api/webhooks/stripe — re-send test event |
| Supabase Cloud | Deprecated for Stratify after data migrate — do not run config push to cloud project for Hetzner auth |
Hetzner Supabase .env | SITE_URL = https://app.stratifyinvest.com, API_EXTERNAL_URL = https://app.stratifyinvest.com/supabase (via hetzner-update-public-urls.sh) |
| Mobile (fastlane) | EXPO_PUBLIC_* now point at app.stratifyinvest.com; build 7 still holds the old Supabase URL, so build 8 must ship before the legacy ventures router is removed — apps/mobile/scripts/testflight-local.sh (EAS is dead; see docs/runbooks/mobile-release.md) |
| Crons | /etc/cron.d/stratify via ./scripts/hetzner-install-crons.sh (source: infra/hetzner/cron-stratify.example); auth CRON_SECRET |
Production data from Supabase Cloud
Destructive on Hetzner `stratify-db` only — requires explicit confirm env:
STRATIFY_CONFIRM_CLOUD_RESTORE=yes \
./scripts/with-stratify-hetzner-supabase-db.sh ./scripts/hetzner-restore-from-cloud.shGuards: linked project ddkpedfjdzdcmgwuxlpb, tunnel to 127.0.0.1:5433, auto backup first. Does not touch Loki (supabase-db / port 5432) or Ecosystem stacks.
E2E checklist (after cutover)
- [ ]
/sign-in— request a 6-digit code, enter it, land on/feed - [ ]
/feed— authenticated - [ ]
/ops— admin role - [ ] Partner console — demo partner
- [ ]
POST /api/webhooks/stripe— Stripe test event - [ ] Cron endpoints with
CRON_SECRET
DSGVO / ops
- Backups: nightly
hetzner-backup-postgres.sh→/data/stratify/backups - EU host: Hetzner nbg1 — document in
docs/compliance/dsgvo-data-flow.mdwhen cutover is permanent - Audit retention: unchanged (chain-hashed, 2y pre-license)
Server power schedule
See `docs/runbooks/hetzner-power-schedule.md` — recommended ON 04:45 / OFF 23:30 Europe/Zurich if you enable overnight shutdown (shared with Ecosystem 03:00 UTC jobs).
Crons (Hetzner)
./scripts/hetzner-install-crons.shLogs: /var/log/stratify-cron.log. Vercel crons removed from apps/web/vercel.json (Hetzner-only).