Self-Hosting Vaultwarden: Setup a Private Bitwarden Server with Docker

Step-by-step tutorial to self-host Vaultwarden (Bitwarden API in Rust) on your VPS using Docker Compose. Secure your password vault today!

System Requirements and Directory Structure

This deployment requires a VPS running a modern Linux distribution (e.g., Ubuntu 22.04 LTS or Debian 12) with Docker and the Docker Compose plugin installed. You also need a fully qualified domain name (FQDN) pointed to your VPS public IP address via an A record.

Create a dedicated directory structure under /opt to store the application data, configuration, and reverse proxy settings:

sudo mkdir -p /opt/vaultwarden/data
sudo mkdir -p /opt/vaultwarden/caddy
sudo chown -R $USER:$USER /opt/vaultwarden
cd /opt/vaultwarden

Docker Compose Configuration

The following docker-compose.yml configures Vaultwarden alongside Caddy as a reverse proxy. Caddy is selected because it automatically provisions and renews TLS certificates via Let's Encrypt, handles HTTP/2 and HTTP/3, and supports WebSocket routing required by Vaultwarden's live sync.

Create the docker-compose.yml file:

version: '3.8'

services:
  vaultwarden:
    image: vaultwarden/server:1.30.5
    container_name: vaultwarden
    restart: always
    environment:
      - WEBSOCKET_ENABLED=true
      - SIGNUPS_ALLOWED=true
      - ADMIN_TOKEN=${ADMIN_TOKEN}
      - DOMAIN=https://${DOMAIN}
      - DATABASE_URL=/data/db.sqlite3
    volumes:
      - ./data:/data
    networks:
      - vaultwarden-net

  caddy:
    image: caddy:2.7-alpine
    container_name: caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/data:/data
      - ./caddy/config:/config
    environment:
      - DOMAIN=${DOMAIN}
    networks:
      - vaultwarden-net
    depends_on:
      - vaultwarden

networks:
  vaultwarden-net:
    driver: bridge

Reverse Proxy and WebSocket Configuration

Vaultwarden uses WebSockets for real-time syncing between clients (browser extensions, desktop, and mobile apps). The reverse proxy must handle standard HTTP traffic and upgrade WebSocket connections properly.

Create the Caddyfile in /opt/vaultwarden/Caddyfile:

{$DOMAIN} {
    log {
        output file /var/log/caddy/access.log {
            roll_size 10mb
            roll_keep 5
        }
    }

    # TLS Configuration
    tls {
        protocols tls1.2 tls1.3
    }

    # Security Headers
    header {
        # Enable HSTS
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        # Prevent Clickjacking
        X-Frame-Options "SAMEORIGIN"
        # Prevent MIME Sniffing
        X-Content-Type-Options "nosniff"
        # Referrer Policy
        Referrer-Policy "same-origin"
        # Content Security Policy for Vaultwarden
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://identity.bitwarden.com; child-src 'self'; connect-src 'self' wss://{$DOMAIN} https://api.pwnedpasswords.com https://identity.bitwarden.com;"
    }

    # Proxy WebSocket traffic
    reverse_proxy /notifications/hub/negotiate vaultwarden:80
    reverse_proxy /notifications/hub vaultwarden:3012

    # Proxy all other traffic
    reverse_proxy vaultwarden:80
}

Environment Configuration

Use a .env file to manage secret variables. Generate a cryptographically secure token for the admin dashboard using openssl:

openssl rand -base64 48

Create the .env file in /opt/vaultwarden/.env and replace yourdomain.com and YOUR_GENERATED_ADMIN_TOKEN with your actual values:

DOMAIN=yourdomain.com
ADMIN_TOKEN=YOUR_GENERATED_ADMIN_TOKEN

Initial Deployment and Verification

Start the stack in detached mode:

docker compose up -d

Verify that both containers are running and healthy:

docker compose ps

Check the startup logs for any initial configuration errors or TLS issues:

docker compose logs -f

At this stage, navigate to https://yourdomain.com in your browser. Register your primary account, verify that you can log in, and check if the database files have been created in ./data/.

Security Hardening Post-Deployment

Disabling Signups

To prevent unauthorized users from registering on your instance, disable signups once you have created your account. Edit /opt/vaultwarden/docker-compose.yml and change the environment variable:

      - SIGNUPS_ALLOWED=false

Apply the configuration change by recreating the container:

docker compose up -d --force-recreate vaultwarden

Securing the Admin Dashboard

The admin dashboard is available at /admin and is authenticated via the ADMIN_TOKEN defined in your .env file. You should restrict access to this endpoint to specific IP addresses.

Update your Caddyfile to restrict access to the /admin path:

    # Restrict Admin Access to trusted IPs (replace with your IP or subnet)
    @blocked_admin {
        path /admin*
        not remote_ip 203.0.113.50 192.168.1.0/24
    }
    respond @blocked_admin "Forbidden" 403

Automated Backups

Vaultwarden stores user data, organizations, attachments, and settings in /opt/vaultwarden/data. Because SQLite databases can be corrupted if backed up mid-transaction, you must perform a safe backup using the SQLite .backup command or by using a dedicated backup container.

Below is a technical backup script that creates a daily snapshot, compresses it, and retains backups for 14 days:

cat << 'EOF' > /opt/vaultwarden/backup.sh
#!/bin/bash
BACKUP_DIR="/opt/vaultwarden/backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DEST_FILE="${BACKUP_DIR}/vaultwarden_backup_${TIMESTAMP}.tar.gz"

mkdir -p "${BACKUP_DIR}"

# Run sqlite3 backup command inside the vaultwarden container
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup '/data/db.sqlite3.backup'"

# Tar the backup db along with other vital directory contents
tar -czf "${DEST_FILE}" \
    -C /opt/vaultwarden/data \
    db.sqlite3.backup config.json rsa_key.der rsa_key.pem attachments

# Remove temporary sqlite backup file
rm -f /opt/vaultwarden/data/db.sqlite3.backup

# Delete backups older than 14 days
find "${BACKUP_DIR}" -type f -name "vaultwarden_backup_*.tar.gz" -mtime +14 -delete
EOF

chmod +x /opt/vaultwarden/backup.sh

To run this backup daily at 2:00 AM, add a cron job:

(crontab -l 2>/dev/null; echo "0 2 * * * /opt/vaultwarden/backup.sh >/dev/null 2>&1") | crontab -