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 -