# Vaultwarden Deployment (VPS Reverse Proxy) Deploy Vaultwarden on the home server, exposed through VPS reverse proxy. Follows the standard VPS reverse proxy pattern. ## Docker Compose ```yaml # /opt/vaultwarden/docker-compose.yml 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=true - WEBSOCKET_ENABLED=true - LOG_FILE=/data/vaultwarden.log - LOG_LEVEL=warn ``` ## Home Nginx Config ```nginx server { listen 3446 ssl; server_name grajmedia.duckdns.org 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; ssl_ciphers HIGH:!aNULL:!MD5; 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; } # WebSocket for live sync (browser extension) 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; } } ``` ## VPS Nginx Config Standard HTTPS proxy pattern. Home nginx expects `Host: vault.graj-media.com` — use `$host` in `proxy_set_header`. ```nginx server { server_name vault.graj-media.com; location / { proxy_pass https://100.93.253.36:3446; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_ssl_verify off; proxy_read_timeout 86400; proxy_buffering off; } listen 443 ssl; ssl_certificate /etc/letsencrypt/live/vault.graj-media.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/vault.graj-media.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; } ``` ## SSL ```bash certbot --nginx -d vault.graj-media.com --non-interactive --agree-tos --email admin@grajmedia.duckdns.org ``` ## Post-Deploy 1. Go to https://vault.graj-media.com and create your account (signups enabled initially) 2. Install Bitwarden browser extension → Settings → Self-hosted → Server URL: `https://vault.graj-media.com` 3. **After account creation, disable signups and set admin token:** ```bash # Disable signups sudo docker stop vaultwarden sudo sed -i 's/SIGNUPS_ALLOWED=true/SIGNUPS_ALLOWED=false/' /opt/vaultwarden/docker-compose.yml # Generate admin token (argon2id hash — do NOT use plain text) ADMIN_TOKEN=$(openssl rand -hex 32) echo "Admin token (save this): $ADMIN_TOKEN" HASHED=$(echo -n "$ADMIN_TOKEN" | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4 | grep 'Encoded:' | awk '{print $2}') echo "ADMIN_TOKEN=$HASHED" | sudo tee -a /opt/vaultwarden/.env sudo docker compose -f /opt/vaultwarden/docker-compose.yml up -d ``` Admin panel: https://vault.graj-media.com/admin — enter the raw admin token (not the hash) to authenticate. 4. **Bitwarden apps setup**: Desktop/Mobile → Settings gear ⚙️ → Self-hosted → Server URL: `https://vault.graj-media.com`. Browser extension → gear ⚙️ → Self-hosted environment → same URL. ## Pitfalls - **WebSocket required for live sync**: Without `/notifications/hub` proxy, the browser extension won't receive real-time updates (password changes, new items from other devices). Always include both the `Upgrade` headers AND the `/notifications/hub/negotiate` endpoint. - **Port conflict check**: Vaultwarden defaults to port 80 internally. Before deploying, check `docker ps` for port conflicts and pick an unused port for the host binding (used 8812 in this setup).