# 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`.
