Files
hermes-config/skills/vps-reverse-proxy/references/vaultwarden.md
T
2026-07-12 10:17:17 -04:00

4.6 KiB

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

# /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

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.

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

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:

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