A password manager is the single most important security tool for any individual or organization. But trusting a third-party cloud service with every password you own creates a concentrated target and a single point of failure. If that service suffers a breach, changes its pricing, or simply goes offline, your entire digital life is affected.

Vaultwarden (formerly bitwarden_rs) is a lightweight, self-hosted implementation of the Bitwarden API written in Rust. It is fully compatible with all official Bitwarden clients (browser extensions, mobile apps, desktop apps, CLI) while using a fraction of the resources of the official Bitwarden server. This guide covers deploying Vaultwarden with Docker, securing it properly, and setting up backups so you never lose access to your vault.

Why Self-Host Your Password Manager?

Consideration Cloud-Hosted (Bitwarden) Self-Hosted (Vaultwarden)
Data location Third-party servers Your server, your control
Cost Free (basic) / $10-$40/yr per user Server costs only
Premium features Paid tiers only All features unlocked
Uptime Managed SLA Your responsibility
Backup Provider handles it You must handle it
Audit Trust the provider Full access to logs and data
Resource usage N/A ~50 MB RAM, minimal CPU

Important: Vaultwarden encrypts all vault data client-side with your master password before it leaves the device. Even if your server is compromised, the attacker only gets encrypted blobs. This is the same security model as official Bitwarden. The server never sees your plaintext passwords.

Docker Deployment

The basic Vaultwarden setup is remarkably simple:

# docker-compose.yml
version: "3.8"
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      # Core settings
      - DOMAIN=https://vault.example.com
      - SIGNUPS_ALLOWED=false
      - INVITATIONS_ALLOWED=true
      - ADMIN_TOKEN=${ADMIN_TOKEN}

      # Email for notifications and 2FA
      - SMTP_HOST=smtp.example.com
      - [email protected]
      - SMTP_PORT=587
      - SMTP_SECURITY=starttls
      - [email protected]
      - SMTP_PASSWORD=${SMTP_PASSWORD}

      # Security settings
      - LOGIN_RATELIMIT_MAX_BURST=5
      - LOGIN_RATELIMIT_SECONDS=60
      - ADMIN_RATELIMIT_MAX_BURST=3
      - ADMIN_RATELIMIT_SECONDS=60

      # Logging
      - LOG_LEVEL=warn
      - LOG_FILE=/data/vaultwarden.log

      # WebSocket support for live sync
      - WEBSOCKET_ENABLED=true
    volumes:
      - vaultwarden_data:/data
    ports:
      - "127.0.0.1:8080:80"
      - "127.0.0.1:3012:3012"

volumes:
  vaultwarden_data:
Warning: Generate a strong ADMIN_TOKEN using openssl rand -base64 48. This token grants full access to the Vaultwarden admin panel, including the ability to delete users and view organization data. Treat it with the same care as a root password.

HTTPS with Reverse Proxy

Vaultwarden must be served over HTTPS. The Bitwarden clients will refuse to connect over plain HTTP (and rightly so). Use a reverse proxy with automatic TLS:

Using Caddy (simplest)

# Caddyfile
vault.example.com {
    # Vaultwarden web interface and API
    reverse_proxy /notifications/hub vaultwarden:3012
    reverse_proxy vaultwarden:80
}

# docker-compose addition for Caddy
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

Using Nginx

# nginx/conf.d/vaultwarden.conf
server {
    listen 443 ssl http2;
    server_name vault.example.com;

    ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    # Main Vaultwarden
    location / {
        proxy_pass http://vaultwarden:80;
        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
    location /notifications/hub {
        proxy_pass http://vaultwarden:3012;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    # Admin panel - restrict access
    location /admin {
        # Optional: restrict to specific IPs
        # allow 192.168.1.0/24;
        # deny all;
        proxy_pass http://vaultwarden:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Admin Panel Configuration

Access the admin panel at https://vault.example.com/admin and enter your ADMIN_TOKEN. Key settings to review:

  • General settings: Verify domain, disable signups, configure invitation policy
  • SMTP settings: Test email delivery (required for 2FA and account recovery)
  • Advanced settings: Configure rate limiting, disable the admin panel after initial setup if desired
# Disable admin panel entirely after setup (environment variable)
ADMIN_TOKEN=""  # Empty string disables the admin panel

# Or use argon2id hashed token for extra security
# Generate with: echo -n "your-admin-password" | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'

Backup Strategy

Your password vault is arguably the most critical piece of data you own. Losing it means losing access to everything. Implement multiple backup layers:

#!/bin/bash
# vaultwarden-backup.sh
set -euo pipefail

BACKUP_DIR="/backups/vaultwarden/$(date +%Y%m%d_%H%M%S)"
VAULTWARDEN_DATA="/var/lib/docker/volumes/vaultwarden_data/_data"
ENCRYPTION_KEY_FILE="/root/.vaultwarden-backup-key"
RETENTION_DAYS=90

mkdir -p "$BACKUP_DIR"

# 1. Copy the SQLite database (primary data store)
# Use sqlite3 backup command for consistency
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup '/data/db-backup.sqlite3'"
docker cp vaultwarden:/data/db-backup.sqlite3 "$BACKUP_DIR/db.sqlite3"
docker exec vaultwarden rm /data/db-backup.sqlite3

# 2. Copy attachments and configuration
docker run --rm \
  -v vaultwarden_data:/source:ro \
  -v "$BACKUP_DIR":/backup \
  alpine sh -c "
    cp -r /source/attachments /backup/attachments 2>/dev/null || true
    cp -r /source/sends /backup/sends 2>/dev/null || true
    cp /source/rsa_key* /backup/ 2>/dev/null || true
    cp /source/config.json /backup/ 2>/dev/null || true
  "

# 3. Create encrypted archive
tar czf - -C "$BACKUP_DIR" . | \
  openssl enc -aes-256-cbc -salt -pbkdf2 \
  -pass file:"$ENCRYPTION_KEY_FILE" \
  -out "$BACKUP_DIR.tar.gz.enc"

# 4. Remove unencrypted backup
rm -rf "$BACKUP_DIR"

# 5. Upload to remote storage
if command -v rclone &> /dev/null; then
  rclone copy "$BACKUP_DIR.tar.gz.enc" remote:backups/vaultwarden/
fi

# 6. Cleanup old local backups
find /backups/vaultwarden -name "*.tar.gz.enc" -mtime +$RETENTION_DAYS -delete

echo "Vaultwarden backup complete: $BACKUP_DIR.tar.gz.enc"
# Restore from encrypted backup
openssl enc -aes-256-cbc -d -salt -pbkdf2 \
  -pass file:/root/.vaultwarden-backup-key \
  -in /backups/vaultwarden/20250415_020000.tar.gz.enc | \
  tar xzf - -C /tmp/vaultwarden-restore/

# Stop Vaultwarden, replace data, restart
docker compose stop vaultwarden
docker run --rm \
  -v vaultwarden_data:/target \
  -v /tmp/vaultwarden-restore:/source:ro \
  alpine sh -c "rm -rf /target/* && cp -r /source/* /target/"
docker compose up -d vaultwarden
Tip: Schedule backups with cron and also perform an export from the Bitwarden client periodically. An encrypted JSON export stored in a secure offline location (like an encrypted USB drive in a safe) provides a last-resort recovery path independent of the server entirely.

Emergency Access

Vaultwarden supports Bitwarden's emergency access feature, which allows trusted contacts to access your vault if you become incapacitated:

  1. The trusted contact requests access through their Bitwarden client
  2. A configurable waiting period begins (e.g., 7 days)
  3. If you do not reject the request within the waiting period, the contact gains access
  4. Access can be read-only (view) or full (takeover)

To enable emergency access, ensure SMTP is configured (notifications require email) and that the feature is not disabled in the admin panel. Each user configures their own emergency contacts through the Bitwarden web vault or client apps.

Security Hardening

Beyond the basic setup, apply these security measures:

1. Disable Registration After Initial Setup

# In docker-compose.yml environment
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=true  # Admin can still invite users

2. Enable Two-Factor Authentication

Vaultwarden supports TOTP, FIDO2/WebAuthn, Duo, and email-based 2FA. Require all users to enable at least one method:

# Enforce 2FA for all organization members
# Configure in admin panel: Settings > Advanced > Require 2FA

3. Fail2Ban Integration

# /etc/fail2ban/filter.d/vaultwarden.conf
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: \. Username:.*$
ignoreregex =

# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
port = 80,443,8080
filter = vaultwarden
action = iptables-allports[name=vaultwarden, chain=FORWARD]
logpath = /path/to/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 14400
findtime = 14400

4. Network-Level Restrictions

# Restrict admin panel to local network only
# In docker-compose.yml environment:
ADMIN_TOKEN=${ADMIN_TOKEN}
# Then in reverse proxy, restrict /admin to trusted IPs

# Or use Docker network isolation
# Only expose Vaultwarden to the reverse proxy network
services:
  vaultwarden:
    networks:
      - proxy_network
    # Remove direct port mapping
    # ports:
    #   - "8080:80"

5. Content Security Policy

# In your reverse proxy, add strict CSP headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com; connect-src 'self' wss://vault.example.com https://haveibeenpwned.com" always;

Client Configuration

All official Bitwarden clients work with Vaultwarden. Configure them by setting the server URL:

Client Configuration
Browser extension Settings gear icon > Self-hosted > Server URL: https://vault.example.com
Desktop app Login screen > Self-hosted > Server URL: https://vault.example.com
Mobile app (iOS/Android) Login screen > Self-hosted > Server URL: https://vault.example.com
CLI bw config server https://vault.example.com
# Bitwarden CLI usage
bw config server https://vault.example.com
bw login [email protected]
bw sync

# Export vault (encrypted)
bw export --format encrypted_json --password "strong-export-password"

# Useful CLI operations
bw list items --search "github"
bw get password "GitHub"
bw generate -ulns --length 32

Monitoring Vaultwarden

Monitor your Vaultwarden instance to ensure it stays healthy:

# Docker health check (add to docker-compose.yml)
services:
  vaultwarden:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/alive"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

Integrate with your monitoring stack (Prometheus, Uptime Kuma) to get alerts when Vaultwarden becomes unavailable. A password manager outage, while not catastrophic (clients cache vaults locally), should be resolved quickly to maintain sync functionality.

Container management platforms like usulnet make it straightforward to monitor Vaultwarden alongside your other self-hosted services, tracking container health, resource usage, and backup status from a single dashboard.

Summary

Vaultwarden is one of the easiest and most impactful self-hosting projects. It uses minimal resources (~50 MB RAM), is compatible with all official Bitwarden clients, and gives you complete control over your most sensitive data. The key requirements are proper HTTPS configuration, robust backups (including offline exports), and security hardening with rate limiting and fail2ban. Set it up once, back it up religiously, and you have a password management solution that depends on nobody but you.