main
Vaultwarden self-host deployment
Reproducible, single-user, zero-knowledge Vaultwarden on Hetzner Cloud + AlmaLinux 10 with rootless Podman quadlets, custom Caddy (DNS-01 + Coraza DetectionOnly), restic backups to Cloudflare R2, and WireGuard-gated admin access.
Plan: ~/.claude/plans/vaultwarden-self-host-sharded-creek.md.
Layout
| Path | Purpose |
|---|---|
cloudbuild.yaml |
Cloud Build pipeline — drives Pulumi + image build + Ansible |
pulumi/ |
Phase 1: Hetzner server, Cloud Firewall, volume, GCP Cloud DNS records (Pulumi Go) |
caddy/ |
Phase 2: custom Caddy build (Containerfile + pinned versions) and the Caddyfile |
ansible/ |
Phase 3: OS hardening, WireGuard server, nftables, fail2ban, msmtp, dnf-automatic, Secret Manager → Podman secrets |
quadlets/ |
Phase 4: rootless Vaultwarden + Caddy quadlet units |
backup/ |
Phase 5: SQLite-snapshot + restic + systemd timer + Healthchecks pings |
runbook/ |
Deployment runbook, cutover checklist, restore drill |
secrets/ |
Docs only — secret material lives in Google Cloud Secret Manager, fetched at deploy time |
Stack
| Layer | Choice |
|---|---|
| Compute | Hetzner Cloud, CX22, AlmaLinux 10 (alma-10 image) |
| IaC | Pulumi (Go) |
| OS config | Ansible |
| Build / deploy pipeline | Google Cloud Build (image build, Trivy scan, Pulumi, Ansible) |
| Secrets | Google Cloud Secret Manager (OIDC via Cloud Build SA; ADC for local operator) |
| Reverse proxy / TLS / WAF | Caddy (custom: googleclouddns DNS-01 + Coraza DetectionOnly) |
| Network filtering | Hetzner Cloud Firewall + host nftables + Fail2Ban |
| Containers | Rootless Podman quadlets, SELinux enforcing |
| DB | SQLite (single-user; no replication required) |
| Updates | dnf-automatic (OS) + podman auto-update (containers) |
| Backups | restic → Cloudflare R2 |
| DNS | GCP Cloud DNS |
| Admin/SSH gating | WireGuard server on the Vaultwarden host |
| Alerting | Healthchecks.io (timer pings) + msmtp email (systemd OnFailure=) |
Build-it-disposably-first
Two Pulumi stacks: disposable (throwaway hostname, no real vault data) and production.
Iterate pulumi destroy && pulumi up on disposable until the stack stands up cleanly
with zero hand-intervention, then promote to production for migration.
Pinned versions
See memory/coraza_caddy_version.md for the Coraza/Caddy version pinning rationale.
Languages
Go
56.7%
Jinja
24.7%
Shell
12.2%
Dockerfile
6.4%