The problem
Spreadsheets do not remember. Every household question I cared about was a question across time: are we on pace, what did we spend in the same month last year, when did that balance start drifting. I wanted one place where every record lands and every dashboard reads from the same tables.
What I built
- A Hetzner Cloud VM in its own project, provisioned with the
hcloudCLI and a firewall that allows SSH and HTTPS only. - Docker Compose with three services: PostgreSQL 16, Metabase pinned to one version, and Caddy for automatic HTTPS.
- Two databases:
metabase_appfor Metabase's own state,brosnan_growthfor everything else. - A read-only
metabase_readerrole with default privileges, so every new schema is readable by dashboards without a grant ceremony. - Schemas for the consulting pipeline (leads, engagements, invoices) and for the household itself (inventory, deadlines, records).
- Nightly
pg_dumpwith 14-day retention and memory limits per container so the OOM killer picks Metabase first, never Postgres.
What it taught me
A database password lives in more places than you think. The reader role's password exists in Postgres, in Metabase's saved connection, and in the environment file. Rotate one and nothing breaks until the next container restart, when every chart fails at once. Rotate all three in one change and restart to prove it.
DNS for the Metabase hostname is set to DNS only, not proxied. Caddy needs direct traffic to issue its certificate. Everything else on the domain goes through Cloudflare.