Vaultwarden Self-Hosted Deployment
Vaultwarden is a Rust reimplementation of the Bitwarden server API. Much lighter on resources than the official server, runs happily on a Pi, and has no paywall on features like 2FA, organisations, or file attachments.
Overview
Vaultwarden is an independent Rust implementation of the Bitwarden server API. It uses a fraction of the RAM and CPU the official server needs, which makes it well suited to self-hosting.
Repository
https://github.com/dani-garcia/vaultwarden
Key Differences from Official Bitwarden
| Feature | Vaultwarden | Official Bitwarden |
|---|---|---|
| Language | Rust | C# (.NET) |
| Resource Usage | ~10-20 MB RAM | ~2-4 GB RAM |
| Database | SQLite, MySQL, PostgreSQL | MS SQL Server, PostgreSQL |
| Containers | Single container | 8+ containers |
| License | GPL-3.0 | AGPL-3.0 + Proprietary |
| Premium Features | All free | Requires paid license |
| Official Support | Community | Bitwarden Inc. |
| Enterprise SSO | ✗ | ✓ |
| Complexity | Low | High |
When to Use Vaultwarden
✓ Good for:
- Personal use
- Small teams (< 50 users)
- Home labs / self-hosting
- Limited resources (Raspberry Pi, VPS)
- Free premium features
✗ Not ideal for:
- Enterprise deployments
- Regulatory compliance requirements
- Need for official support
- Enterprise SSO requirements
System Requirements
Minimum Requirements
- CPU: 1 core
- RAM: 256 MB
- Storage: 1 GB
- OS: Linux, Windows, macOS
- Docker: 20.10+
Recommended Requirements
- CPU: 2 cores
- RAM: 512 MB
- Storage: 5 GB SSD
- Reverse proxy: NGINX, Caddy, Traefik
- SSL Certificate: Let’s Encrypt or valid cert
Vaultwarden will run on something as small as a Pi Zero.
Installation
Vaultwarden publishes container images and nothing else. There are no prebuilt binaries on the releases page, so the options are the image below or building it yourself from source.
Docker run
docker run -d --name vaultwarden \
-e ROCKET_PORT=8080 \
-v /vw-data:/data \
-p 8080:8080 \
--restart unless-stopped \
vaultwarden/server:latest Docker Compose
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- DOMAIN=https://vault.example.com
- SIGNUPS_ALLOWED=false
- INVITATIONS_ALLOWED=true
- SHOW_PASSWORD_HINT=false
- WEBSOCKET_ENABLED=true
- SMTP_HOST=smtp.example.com
- SMTP_FROM=vaultwarden@example.com
- SMTP_PORT=587
- SMTP_SECURITY=starttls
- SMTP_USERNAME=vaultwarden@example.com
- SMTP_PASSWORD=smtp_password
- ADMIN_TOKEN=your_secure_admin_token
- LOG_FILE=/data/vaultwarden.log
- LOG_LEVEL=info
volumes:
- ./vw-data:/data
ports:
- "8080:80"
- "3012:3012" # WebSocket port docker compose up -d Configuration
Environment Variables
Core settings:
# Domain (REQUIRED for proper function)
DOMAIN=https://vault.example.com
# Database (defaults to SQLite)
# For SQLite (default)
DATABASE_URL=data/db.sqlite3
# For MySQL
DATABASE_URL=mysql://user:password@host:port/database
# For PostgreSQL
DATABASE_URL=postgresql://user:password@host:port/database
# Rocket (web server) settings
ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8080
ROCKET_WORKERS=10
# WebSocket support (for live sync)
WEBSOCKET_ENABLED=true
WEBSOCKET_ADDRESS=0.0.0.0
WEBSOCKET_PORT=3012 Security settings:
# Disable new user registration (recommended)
SIGNUPS_ALLOWED=false
# Allow invitations from existing users
INVITATIONS_ALLOWED=true
# Require email verification
SIGNUPS_VERIFY=true
# Disable password hints
SHOW_PASSWORD_HINT=false
# Enforce 2FA for all users (optional)
REQUIRE_DEVICE_EMAIL=true
# Admin token (for /admin panel)
ADMIN_TOKEN=your_secure_random_token SMTP (Email) settings:
SMTP_HOST=smtp.example.com
SMTP_FROM=vaultwarden@example.com
SMTP_FROM_NAME=Vaultwarden
SMTP_PORT=587
SMTP_SECURITY=starttls # or 'force_tls' or 'off'
SMTP_USERNAME=vaultwarden@example.com
SMTP_PASSWORD=smtp_password
SMTP_TIMEOUT=15 Advanced settings:
# File upload size limit (in MB)
FILE_SIZE_LIMIT=512
# Attachment folder path
ATTACHMENTS_FOLDER=/data/attachments
# Icon service (for website favicons)
ICON_SERVICE=internal # or 'duckduckgo', 'google'
# Disable icon downloads
DISABLE_ICON_DOWNLOAD=false
# Enable U2F (for FIDO2 keys)
ENABLE_U2F=true
# Logging
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info # or debug, trace, warn, error
EXTENDED_LOGGING=true
# Emergency access
EMERGENCY_ACCESS_ALLOWED=true Generate Admin Token
Create secure admin token:
openssl rand -base64 48 Use this as ADMIN_TOKEN value.
Configuration File (Alternative)
Instead of environment variables, use config.json:
{
"domain": "https://vault.example.com",
"signups_allowed": false,
"invitations_allowed": true,
"show_password_hint": false,
"admin_token": "your_secure_token",
"smtp_host": "smtp.example.com",
"smtp_from": "vaultwarden@example.com",
"smtp_port": 587,
"smtp_security": "starttls",
"smtp_username": "vaultwarden@example.com",
"smtp_password": "smtp_password"
} Mount in Docker:
volumes:
- ./config.json:/data/config.json Reverse Proxy Configuration
NGINX
server {
listen 443 ssl http2;
server_name vault.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
client_max_body_size 525M;
location / {
proxy_pass http://localhost:8080;
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 support
location /notifications/hub {
proxy_pass http://localhost:3012;
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;
}
location /notifications/hub/negotiate {
proxy_pass http://localhost:8080;
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;
}
}
# HTTP redirect
server {
listen 80;
server_name vault.example.com;
return 301 https://$server_name$request_uri;
} Caddy (Simplest)
Caddyfile:
vault.example.com {
reverse_proxy localhost:8080 {
header_up X-Real-IP {remote_host}
}
reverse_proxy /notifications/hub localhost:3012
} Caddy automatically handles SSL with Let’s Encrypt!
Traefik (Docker)
docker-compose.yml with Traefik labels:
version: '3'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- DOMAIN=https://vault.example.com
- WEBSOCKET_ENABLED=true
volumes:
- ./vw-data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
# WebSocket
- "traefik.http.routers.vaultwarden-ws.rule=Host(`vault.example.com`) && Path(`/notifications/hub`)"
- "traefik.http.routers.vaultwarden-ws.entrypoints=websecure"
- "traefik.http.routers.vaultwarden-ws.tls.certresolver=letsencrypt"
- "traefik.http.services.vaultwarden-ws.loadbalancer.server.port=3012"
networks:
- traefik
networks:
traefik:
external: true Admin Panel
Access at: https://vault.example.com/admin
Login with admin token (set via ADMIN_TOKEN)
Admin Panel Features
- Users: View, delete, deauthorize users
- Invitations: View/delete pending invitations
- Organizations: View organizations
- Diagnostics: Check config, connectivity
- Settings: Modify runtime configuration
Disable Admin Panel (After Setup)
For security, disable after initial configuration:
# Remove or comment out ADMIN_TOKEN
# ADMIN_TOKEN= Or use fail2ban to limit access attempts.
Database Options
SQLite (Default)
Pros:
- Zero configuration
- Perfect for small deployments
- Single file backup
Cons:
- Not suitable for high concurrency
- Limited to single server
Location:
/data/db.sqlite3 MySQL / MariaDB
Setup:
Create database:
CREATE DATABASE vaultwarden CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'vaultwarden'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON vaultwarden.* TO 'vaultwarden'@'%'; FLUSH PRIVILEGES;Configure Vaultwarden:
DATABASE_URL=mysql://vaultwarden:password@mysql:3306/vaultwarden
PostgreSQL
Setup:
Create database:
CREATE DATABASE vaultwarden; CREATE USER vaultwarden WITH PASSWORD 'password'; GRANT ALL PRIVILEGES ON DATABASE vaultwarden TO vaultwarden;Configure Vaultwarden:
DATABASE_URL=postgresql://vaultwarden:password@postgres:5432/vaultwarden
Backup and Restore
Backup with SQLite
Manual backup:
# Stop Vaultwarden
docker stop vaultwarden
# Backup data directory
tar -czf vaultwarden-backup-$(date +%Y%m%d).tar.gz ./vw-data/
# Start Vaultwarden
docker start vaultwarden Automated backup script:
#!/bin/bash
# vaultwarden-backup.sh
BACKUP_DIR="/backups/vaultwarden"
DATA_DIR="./vw-data"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Stop container for consistent backup
docker stop vaultwarden
# Create backup
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz" "$DATA_DIR"
# Start container
docker start vaultwarden
# Keep last 30 backups
find "$BACKUP_DIR" -name "vaultwarden-*.tar.gz" -mtime +30 -delete
echo "Backup completed: $BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz" Cron job (daily at 2 AM):
0 2 * * * /home/user/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1 Restore from Backup
# Stop Vaultwarden
docker stop vaultwarden
# Remove existing data
rm -rf ./vw-data
# Extract backup
tar -xzf vaultwarden-20250106.tar.gz
# Start Vaultwarden
docker start vaultwarden Backup with MySQL/PostgreSQL
Use standard database backup tools:
MySQL:
mysqldump -u vaultwarden -p vaultwarden > vaultwarden-$(date +%Y%m%d).sql PostgreSQL:
pg_dump -U vaultwarden vaultwarden > vaultwarden-$(date +%Y%m%d).sql Security Hardening
1. Disable User Registration
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=true # Allow existing users to invite 2. Enforce 2FA
In admin panel:
- Access
/admin - Navigate to Settings
- Enable relevant 2FA options
3. Implement Fail2Ban
Create filter:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex = Create jail:
# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /path/to/vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400 4. Restrict Admin Panel
NGINX - Allow only from specific IP:
location /admin {
allow 192.168.1.0/24;
deny all;
proxy_pass http://localhost:8080;
# ... other proxy settings
} 5. Enable Extended Logging
LOG_LEVEL=info
EXTENDED_LOGGING=true Monitoring
Check Vaultwarden Status
Docker:
docker ps | grep vaultwarden
docker logs vaultwarden
docker stats vaultwarden Health Check
HTTP endpoint:
curl -I https://vault.example.com/alive Expected: HTTP/1.1 200 OK
Prometheus Monitoring
Vaultwarden doesn’t expose Prometheus metrics natively, but you can monitor via:
- Blackbox exporter - HTTP endpoint monitoring
- Docker exporter - Container metrics
- Log exporters - Parse logs for metrics
Updates
Update Docker Container
# Pull latest image
docker pull vaultwarden/server:latest
# Stop and remove old container
docker stop vaultwarden
docker rm vaultwarden
# Start with new image (using same docker run command)
docker run -d --name vaultwarden \
-v /vw-data:/data \
-p 8080:8080 \
vaultwarden/server:latest With Docker Compose:
docker compose pull
docker compose up -d Migrating from Official Bitwarden
Export from Bitwarden
- Login to Bitwarden web vault
- Tools → Export Vault
- Select format:
.json - Download export
Import to Vaultwarden
- Login to Vaultwarden web vault
- Tools → Import Data
- Select format:
Bitwarden (json) - Upload export file
Note: Organization data requires manual re-creation.
Troubleshooting
WebSocket Not Working
Symptoms: Live sync doesn’t work, requires manual refresh
Fix:
- Ensure
WEBSOCKET_ENABLED=true - Check reverse proxy WebSocket configuration
- Verify port 3012 is accessible
Email Not Sending
Check SMTP settings:
# Test SMTP from container
docker exec -it vaultwarden sh
telnet smtp.example.com 587 Common issues:
- Wrong SMTP port
SMTP_SECURITYmismatch- Firewall blocking outbound SMTP
- ISP blocking port 25
Database Locked (SQLite)
Cause: SQLite doesn’t handle concurrent writes well
Solutions:
- Migrate to MySQL/PostgreSQL
- Reduce concurrent user operations
- Increase
ROCKET_WORKERS
High Memory Usage
Normal memory usage:
- SQLite: 10-50 MB
- MySQL: 20-100 MB
- Heavy usage: Up to 200 MB
If excessive:
- Check for large attachments
- Review
FILE_SIZE_LIMIT - Monitor database growth
Comparison: Vaultwarden vs Official Bitwarden
| Feature | Vaultwarden | Official Bitwarden |
|---|---|---|
| 2FA | ✓ TOTP, YubiKey, Duo, Email | ✓ TOTP, YubiKey, Duo, Email |
| Organizations | ✓ Free | ✓ Paid ($3-5/user/month) |
| File Attachments | ✓ Free (1GB/user) | ✓ Paid (1GB-100GB) |
| Emergency Access | ✓ Free | ✓ Paid |
| Directory Sync | ✗ | ✓ Paid |
| Enterprise SSO | ✗ | ✓ Paid |
| Event Logs | ✓ Basic | ✓ Advanced (paid) |
| Support | Community | Official |
| Compliance Certs | ✗ | ✓ SOC 2, HIPAA |
Additional Resources
- Official Wiki: https://github.com/dani-garcia/vaultwarden/wiki
- Community Forum: https://github.com/dani-garcia/vaultwarden/discussions
- Docker Hub: https://hub.docker.com/r/vaultwarden/server
Key Topics
Core Configuration
- Docker Compose Configuration for Password Managers - Complete Docker setup patterns
- Password Manager Backup and Recovery Strategies - Backup automation and disaster recovery
- Database Options for Self-Hosted Applications - SQLite, MySQL, PostgreSQL comparison
- SMTP Email Configuration for Self-Hosted Services - Email delivery setup
- Reverse Proxy Configuration for Web Services - NGINX, Caddy, Traefik configurations
Related Services
- Bitwarden Self-Hosted Deployment - Official enterprise-grade alternative
- SSL Certificate Management - Certificate management and automation