# Vaultwarden Deployment (rayserver)

Complete worked example for deploying Vaultwarden (Bitwarden-compatible password manager) on rayserver infrastructure.

## Architecture

```
Browser → VPS (vault.graj-media.com:443, nginx SSL)
           ↳ Tailscale → Home server (100.93.253.36:3446, nginx SSL)
                          ↳ Vaultwarden (127.0.0.1:8812, Docker)
```

## Docker Compose

```yaml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - /mnt/seagate8tb/docker/vaultwarden:/data
    ports:
      - "127.0.0.1:8812:80"
    environment:
      - DOMAIN=https://vault.graj-media.com
      - SIGNUPS_ALLOWED=false
      - WEBSOCKET_ENABLED=true
      - LOG_FILE=/data/vaultwarden.log
      - LOG_LEVEL=warn
```

**Key settings:**
- `DOMAIN` — must match the public URL for correct invite links and CORS
- `SIGNUPS_ALLOWED` — enable temporarily for account creation, disable after
- `WEBSOCKET_ENABLED` — required for live sync with browser extensions
- Bind to `127.0.0.1` — all access goes through nginx

## Nginx WebSocket Configuration

Vaultwarden uses WebSocket for the notifications hub. The nginx config needs a dedicated location block for `/notifications/hub` with `Upgrade` and `Connection` headers, plus a separate block for the negotiate endpoint:

```nginx
server {
    listen 3446 ssl;
    server_name vault.graj-media.com;

    ssl_certificate /etc/letsencrypt/live/grajmedia.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grajmedia.duckdns.org/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    client_max_body_size 128M;

    location / {
        proxy_pass http://127.0.0.1:8812;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /notifications/hub {
        proxy_pass http://127.0.0.1:8812;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /notifications/hub/negotiate {
        proxy_pass http://127.0.0.1:8812;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }
}
```

Without the WebSocket blocks, browser extensions can't maintain a live sync connection — they'll work for manual syncs but won't push changes in real-time.

## Admin Token Setup

After first deployment, set the admin token via Docker environment variable. The token MUST be hashed with argon2id:

```bash
# Generate random token
ADMIN_TOKEN=$(openssl rand -hex 32)

# Hash with argon2id using the Vaultwarden container
HASHED=$(echo -n "$ADMIN_TOKEN" | sudo docker exec -i vaultwarden /vaultwarden hash)

# Add to docker-compose.yml environment section
#   - ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
# Then restart: sudo docker compose up -d
```

Access admin panel at `https://vault.graj-media.com/admin` — use the raw (unhashed) token to log in.

## Post-Deploy Steps

1. **Open signups temporarily**: set `SIGNUPS_ALLOWED=true`, restart container
2. **Create user account**: visit https://vault.graj-media.com, register
3. **Disable signups**: set `SIGNUPS_ALLOWED=false`, restart container
4. **Configure Bitwarden apps**: Settings → Self-hosted → Server URL: `https://vault.graj-media.com`

## Bitwarden Client Setup

All official Bitwarden clients support self-hosted servers:
- **Browser extension**: Settings → Self-Hosted Environment → Server URL
- **Desktop app**: Settings → Self-Hosted Environment
- **Mobile (iOS/Android)**: Settings → Self-Hosted Environment

Enter `https://vault.graj-media.com` as the server URL. No trailing slash.
