Deploying a Self-Hosted Password Manager with Vaultwarden and a Reverse Proxy

Deploying a Self-Hosted Password Manager with Vaultwarden and a Reverse Proxy

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

Handing your passwords to a third-party SaaS provider has always made me a little uneasy. Vaultwarden — a lightweight, open-source Bitwarden-compatible server written in Rust — gives you full control over your credential vault without sacrificing the polished Bitwarden clients you already know. In this tutorial I'll walk you through a production-ready deployment using Docker Compose and Caddy as the reverse proxy, including automatic HTTPS, persistent storage, and a few security tweaks that most guides skip.

Why Vaultwarden Instead of the Official Bitwarden Server?

The official Bitwarden server is a multi-container beast that wants gigabytes of RAM. Vaultwarden is a single container that idles comfortably under 20 MB of memory. I've been running it on a $6/month DigitalOcean Droplet for over two years without issues. It implements virtually the entire Bitwarden API — including organisations, TOTP, emergency access, and the Send feature — so every official Bitwarden client (browser extension, Android, iOS, desktop) connects to it without modification.

The one thing you need is HTTPS. Bitwarden clients refuse to connect to plain HTTP, which is actually a security feature you want. That's where the reverse proxy comes in.

Prerequisites

If you need a reliable place to run this, create a DigitalOcean account — their $6 Basic Droplet (1 vCPU, 1 GB RAM) is more than enough for a personal or family vault.

Project Directory Layout

I keep everything under /opt/vaultwarden. Create the directories now:

sudo mkdir -p /opt/vaultwarden/{data,caddy/data,caddy/config}
cd /opt/vaultwarden

The Docker Compose File

Below is the complete docker-compose.yml. I'm using Caddy as the reverse proxy because it handles Let's Encrypt certificate issuance and renewal entirely automatically — no certbot cron jobs, no manual renewal scripts. I prefer Caddy over Nginx Proxy Manager for minimal setups like this because there's one fewer container and the Caddyfile syntax is genuinely readable.

cat > /opt/vaultwarden/docker-compose.yml <<'EOF'
version: "3.9"

networks:
  proxy:
    driver: bridge

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    networks:
      - proxy
    volumes:
      - ./data:/data
    environment:
      DOMAIN: "https://vault.yourdomain.com"
      SIGNUPS_ALLOWED: "false"          # disable after creating your account
      INVITATIONS_ALLOWED: "true"
      WEBSOCKET_ENABLED: "true"
      LOG_LEVEL: "warn"
      EXTENDED_LOGGING: "true"

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"   # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/data:/data
      - ./caddy/config:/config
EOF
Watch out: Leave SIGNUPS_ALLOWED: "true" only long enough to create your first account. Flip it to false and run docker compose up -d again immediately after. An open registration endpoint is an invitation for strangers to fill your vault server with junk — or worse.

Writing the Caddyfile

Caddy's configuration is intentionally terse. The reverse_proxy directive forwards traffic to Vaultwarden, and the /notifications/hub path handles WebSocket connections for real-time vault sync across devices:

cat > /opt/vaultwarden/Caddyfile <<'EOF'
vault.yourdomain.com {
    encode gzip

    # WebSocket endpoint for live sync
    reverse_proxy /notifications/hub vaultwarden:3012

    # Everything else goes to the main Vaultwarden HTTP port
    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "no-referrer"
        -Server
    }
}
EOF

Replace vault.yourdomain.com with your actual subdomain in both the docker-compose.yml environment variable and the Caddyfile. Caddy will automatically obtain and renew a Let's Encrypt TLS certificate for that domain the first time it starts, as long as port 80 is reachable for the ACME HTTP-01 challenge.

Bringing the Stack Up

cd /opt/vaultwarden
docker compose pull
docker compose up -d
docker compose logs -f

Watch the logs for a line from Caddy like certificate obtained successfully. It usually takes under 30 seconds. Once you see it, navigate to https://vault.yourdomain.com in your browser. You should land on the Vaultwarden login page with a green padlock.

Tip: If Caddy gets stuck trying to obtain a certificate, the most common culprits are (1) your DNS A record hasn't propagated yet — check with dig +short vault.yourdomain.com — or (2) port 80 is blocked by a cloud firewall rule separate from UFW. DigitalOcean, for example, has both a Droplet-level firewall and a cloud firewall that you need to configure independently.

Creating Your First Account and Locking Down Registration

Register your admin account through the web UI at https://vault.yourdomain.com/#/register. Once that's done, immediately update the compose file:

# Edit SIGNUPS_ALLOWED to false in docker-compose.yml, then:
docker compose up -d vaultwarden

Vaultwarden also ships an admin panel at /admin. To enable it, generate a secure token and add it to your environment block:

# Generate a random admin token (do not use this exact string)
openssl rand -base64 48

# Add to docker-compose.yml environment section:
# ADMIN_TOKEN: "paste-your-generated-token-here"

docker compose up -d vaultwarden

The admin panel at https://vault.yourdomain.com/admin lets you manage users, send invitations, view diagnostics, and force 2FA enrollment — all without touching the command line again.

Persistent Backups

All Vaultwarden data lives in /opt/vaultwarden/data. The most critical file is data/db.sqlite3. I back this up nightly with a simple cron job that copies it to an off-site object store, but at minimum you should snapshot the entire data directory regularly. If you're on DigitalOcean, Droplet Backups cover the whole volume automatically for 20% of the Droplet cost.

# Add to root's crontab (crontab -e)
# Runs at 02:00 daily, keeps 7 days of backups
0 2 * * * sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '/opt/vaultwarden/data/db-backup-$(date +\%F).sqlite3'" && find /opt/vaultwarden/data -name 'db-backup-*.sqlite3' -mtime +7 -delete

Connecting the Bitwarden Client Apps

Every official Bitwarden client supports a custom server URL. In the browser extension or mobile app, find the Self-hosted environment option before logging in and enter your full URL: https://vault.yourdomain.com. That's it. Your existing Bitwarden browser extensions, the Android app, the iOS app, and the desktop clients all work without any additional configuration. The experience is identical to the cloud version.

Optional: Enable Fail2ban for Brute-Force Protection

Vaultwarden logs failed logins in /opt/vaultwarden/data/vaultwarden.log. If you have fail2ban installed on the host, you can watch that file and ban offending IPs. This is especially important if your vault is publicly accessible. I cover the full fail2ban setup in the fail2ban and CrowdSec hardening tutorial.

Wrapping Up

You now have a fully operational, HTTPS-secured, Bitwarden-compatible password manager running on your own infrastructure — no subscription fees, no third-party data custody. The whole stack uses under 50 MB of RAM at idle, which means it sits happily alongside other services on even the smallest VPS.

Two natural next steps from here: enable TOTP two-factor authentication on your Vaultwarden account (Settings → Two-step Login in the web vault), and set up Watchtower to keep the vaultwarden/server image updated automatically — critical for a security-sensitive application like this. Both are low-effort additions that meaningfully raise the security bar on your self-hosted vault.

DigitalOcean

Discussion

```