# Vaultwarden Deployment Pattern Standard for deploying a private password manager on this infrastructure. Everything behind Tailscale, zero public exposure. ## Prerequisites - Docker and Docker Compose installed - A Tailscale-assigned IP for the host (for DOMAIN config) - UFW port opened for the chosen port ## Service layout ``` ~/docker/vaultwarden/ ├── docker-compose.yml ← pinned version, never :latest ├── .env ← ADMIN_TOKEN (generated via openssl rand -base64 48) ├── data/ ← SQLite DB, RSA keys (backed up every 15 min to S3) └── CHANGELOG.md ← every config change logged ``` ## docker-compose.yml ```yaml services: vaultwarden: image: vaultwarden/server:1.33.2 container_name: vaultwarden restart: unless-stopped ports: - "8080:80" volumes: - ./data:/data environment: - DOMAIN=http://100.71.155.7:8080 - ADMIN_TOKEN=${ADMIN_TOKEN} - SIGNUPS_ALLOWED=false - INVITATIONS_ALLOWED=true env_file: - .env ``` ## Deployment steps ```bash # Generate admin token ADMIN_TOKEN=$(openssl rand -base64 48) echo "ADMIN_TOKEN=$ADMIN_TOKEN" > ~/docker/vaultwarden/.env # Create compose file # Start container cd ~/docker/vaultwarden && docker compose up -d # Allow through firewall ufw allow 8080/tcp comment 'Vaultwarden' # Verify locally curl -s http://localhost:8080/ | head -5 ``` ## DNS and DOMAIN The DOMAIN env var **must** match how the client accesses it. The web vault is a JavaScript SPA that uses this to build API URLs. Common options: | Access method | DOMAIN value | |---|---| | Localhost | `http://localhost:8080` | | Tailscale IP | `http://100.x.x.x:8080` | | Public domain (HTTPS) | `https://vaultwarden.example.com` | | Tailscale Serve (HTTPS) | `https://100.x.x.x` | | Caddy reverse proxy (HTTPS) | `https://vault.iamgmb.com` | **Current deployment (Jul 12, 2026):** Caddy on Core reverse-proxies `vault.iamgmb.com` → `localhost:8080` with auto Let's Encrypt TLS. DNS record: `A vault.iamgmb.com → 152.53.192.33` (Cloudflare proxied, TTL 120). If the DOMAIN is wrong, the web vault shows a spinning loader forever and `docker logs vaultwarden` will NOT show request errors (the browser makes the API calls, not the server). Fix by updating DOMAIN and restarting. ## Getting started as admin 1. Navigate to `http://100.71.155.7:8080` from a Tailscale-connected device (or `https://vault.iamgmb.com` from anywhere) 2. Create your account (first user to register becomes the admin) 3. Navigate to the admin panel (uses ADMIN_TOKEN) 4. Disable new user signups in admin panel (already set in compose, but verify) ## iOS Browser Issue Safari on iOS blocks plain HTTP connections to LAN/Tailscale IPs. Use Chrome on iOS, or use the HTTPS endpoint `https://vault.iamgmb.com` which works on all browsers including Safari. ## Backup The entire `~/docker/vaultwarden/` directory (including `data/`) is synced to S3 every 15 min via `hermes-live-sync.sh`. The database is SQLite — this means WAL files also sync. ## Pitfalls - **DOMAIN must be set correctly** — wrong DOMAIN = spinner that never resolves. This is the #1 deployment mistake. - **Port must be opened in UFW** — Tailscale gets traffic to the server, but UFW still needs to permit the port. - **ADMIN_TOKEN plaintext warning is cosmetic** — Vaultwarden warns about plaintext admin tokens at startup. This is normal for self-hosted deployments. - **First user = admin** — The first registered account becomes the admin console user. - **Safari blocks HTTP locally on iOS** — Use Chrome or the HTTPS endpoint instead. - **Caddy must have handle for vault.iamgmb.com** — Add to `/etc/caddy/Caddyfile`: ``` vault.iamgmb.com { reverse_proxy localhost:8080 } ``` Then `caddy fmt --overwrite /etc/caddy/Caddyfile && systemctl reload caddy`.