
Why Self-Host a Password Manager with Vaultwarden
Password managers are essential for digital security, but cloud-based services introduce trust dependencies. Vaultwarden, a lightweight, compatible rewrite of Bitwarden’s server in Rust, offers complete data sovereignty. It consumes under 256MB of RAM on a Raspberry Pi, supports unlimited users, and integrates seamlessly with all official Bitwarden clients—mobile, desktop, browser extensions, and CLI. Unlike Bitwarden’s official server, which requires MSSQL and 2GB+ RAM, Vaultwarden runs on SQLite or MariaDB, making it ideal for low-resource environments like a $5 VPS or a home NAS. This guide covers installation, configuration, backups, hardening, and advanced features.
Prerequisites and Server Requirements
Minimum Hardware
- CPU: 1 core (ARM or x86)
- RAM: 512MB (256MB usable after OS overhead)
- Storage: 1GB for Vaultwarden + database, plus space for attachments (if enabled)
- Network: Stable internet connection; public IP or dynamic DNS if remote access is needed
Software Requirements
- OS: Debian 12, Ubuntu 22.04+, or Alpine Linux (recommended for minimal footprint)
- Docker and
docker-compose(or Podman with podman-compose) - Reverse Proxy: Nginx or Caddy (for HTTPS with Let’s Encrypt)
- Domain or Subdomain: vault.yourdomain.com (or a DuckDNS free domain)
Installing Vaultwarden with Docker
Step 1: Prepare the Environment
sudo apt update && sudo apt upgrade -y
sudo apt install docker.io docker-compose -y
sudo systemctl enable --now docker
Step 2: Create Directory Structure
mkdir -p /opt/vaultwarden && cd /opt/vaultwarden
mkdir -p data backups
Step 3: Write docker-compose.yml
version: '3.8'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- SIGNUPS_ALLOWED=false
- DOMAIN=https://vault.yourdomain.com
- LOG_FILE=/data/vaultwarden.log
- LOG_LEVEL=warn
- EXTENDED_LOGGING=false
volumes:
- ./data:/data
ports:
- "127.0.0.1:8080:80"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
Critical Environment Variables:
SIGNUPS_ALLOWED=false– disable open registration once admin account is createdDOMAIN– must match your reverse proxy domain exactly; affects WebSocket and attachment linksLOG_FILE– enable logging to file for troubleshootingIP_HEADER– set toX-Forwarded-Forwhen behind a proxy
Step 4: Deploy the Container
docker-compose pull
docker-compose up -d
docker logs vaultwarden -f # verify startup
Reverse Proxy Configuration with Nginx
Install Nginx and Certbot
sudo apt install nginx certbot python3-certbot-nginx -y
Create Nginx Site Configuration
server {
listen 80;
server_name vault.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name vault.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/vault.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
client_max_body_size 128M;
location / {
proxy_pass http://127.0.0.1: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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub {
proxy_pass http://127.0.0.1:3012;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub/negotiate {
proxy_pass http://127.0.0.1:8080;
}
}
Note: The /notifications/hub block enables real-time sync across devices. Without it, clients poll every 30 seconds. Vaultwarden exposes a separate WebSocket port (3012 by default) – add WEBSOCKET_PORT=3012 to the environment if needed.
Obtain SSL Certificate
sudo certbot --nginx -d vault.yourdomain.com --non-interactive --agree-tos -m youremail@domain.com
Enable Auto-Renewal
sudo systemctl enable certbot.timer
Initial Setup and Admin Account
Access the Web Vault
Navigate to https://vault.yourdomain.com. Create the first account. This automatically becomes an admin if no other users exist.
Enable Admin Panel
Add to docker-compose.yml environment:
- ADMIN_TOKEN=YourSuperSecureRandomToken
Generate a strong token: openssl rand -base64 48
Restart Vaultwarden: docker-compose down && docker-compose up -d
Access admin panel at https://vault.yourdomain.com/admin. From here you can:
- Disable new signups permanently
- View and delete users
- Enable or disable 2FA per user
- Export a JSON backup of all user data
- Configure SMTP for password resets and invitations
Mail Configuration for Invitations and Notifications
Without SMTP, users cannot receive invitations or reset passwords. Add to docker-compose.yml environment:
- SMTP_HOST=smtp.gmail.com
- SMTP_PORT=587
- SMTP_SSL=true
- SMTP_FROM=vaultwarden@yourdomain.com
- SMTP_USERNAME=your_app_email@gmail.com
- SMTP_PASSWORD=your_app_specific_password
- SMTP_AUTH_METHOD=PLAIN
For Gmail, use an App Password (enable 2FA on the Google account first). For sendgrid or mailgun, adjust SMTP_HOST accordingly. Restart container after changes.
Backup Strategies
Full Backup Script (Run Daily via Cron)
#!/bin/bash
BACKUP_DIR="/opt/vaultwarden/backups"
DATE=$(date +%Y-%m-%d)
# Stop container for consistency
docker stop vaultwarden
# Backup database and config
tar -czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz"
-C /opt/vaultwarden data/db.sqlite3
data/attachments
data/config.json
docker-compose.yml
# Restart container
docker start vaultwarden
# Encrypt backup with GPG (optional)
gpg --output "$BACKUP_DIR/vaultwarden-$DATE.tar.gz.gpg"
--symmetric --cipher-algo AES256 "$BACKUP_DIR/vaultwarden-$DATE.tar.gz"
rm "$BACKUP_DIR/vaultwarden-$DATE.tar.gz"
# Keep backups for 30 days
find "$BACKUP_DIR" -name "*.gpg" -type f -mtime +30 -delete
Offsite Backup with Rclone
rclone sync /opt/vaultwarden/backups remote:my-bucket/vaultwarden --verbose
Set up rclone with your cloud provider (GDrive, S3, B2) using rclone config.
Automated Backup in docker-compose
Add a backup service:
backup:
image: alpine:latest
volumes:
- ./data:/data
- ./backups:/backups
command: |
sh -c "apk add sqlite && sqlite3 /data/db.sqlite3 '.backup /backups/db.sqlite3' && tar czf /backups/fullbackup-$(date +%Y%m%d).tar.gz -C /data ."
cron: "0 3 * * *"
Hardening for Production
Enable Two-Factor Authentication (2FA)
From admin panel or each user’s account settings: enable TOTP (Google Authenticator) or FIDO2 WebAuthn (YubiKey). Users should enforce this.
# In environment
- REQUIRE_2FA=true
Use Fail2ban with Vaultwarden Logs
sudo apt install fail2ban -y
sudo nano /etc/fail2ban/jail.local
Add:
[vaultwarden]
enabled = true
port = http,https
filter = vaultwarden
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 3600
Create filter:
sudo nano /etc/fail2ban/filter.d/vaultwarden.conf
Content:
[Definition]
failregex = ^.*Invalid password for user.*$
ignoreregex =
Restart fail2ban: sudo systemctl restart fail2ban
Network Security
- Bind Vaultwarden only to 127.0.0.1 (as shown in docker-compose), never expose port 80 directly
- Use ufw:
sudo ufw allow 22/tcp && sudo ufw allow 443/tcp && sudo ufw enable - Enable automatic security updates:
sudo dpkg-reconfigure --priority=low unattended-upgrades
Advanced Features
Organization and Sharing
Users can create Organizations within the web vault. Enable administrators, owners, managers, and users with granular permissions. Use Collections to group passwords logically (e.g., “IT Team,” “Marketing”).
Emergency Access
Vaultwarden supports Bitwarden’s emergency access feature. One user can designate another as a trusted emergency contact with a configurable activation delay (1–30 days). Enable in admin panel or user settings.
Self-Hosted Attachments
By default, file attachments are stored in /data/attachments. To limit size, set FILE_UPLOAD_LIMIT_MB=100 in environment. For larger attachments, consider an external object store (S3-compatible) via the ATTACHMENT_STORAGE_BACKEND experimental variable.
Disable Unused Features
Reduce attack surface:
- SHOW_PASSWORD_HINT=false
- DISABLE_ICON_DOWNLOAD=true # saves bandwidth
- PASSWORD_ITERATIONS=100000 # higher = more secure but slower
Monitoring and Alerts
Health Check Endpoint
Vaultwarden exposes a health check at /alive. Set up monitoring with UptimeRobot or a local script:
curl -f https://vault.yourdomain.com/alive || notify-send "Vaultwarden down!"
Logging
- Enable structured JSON logging:
LOG_FORMAT=json - Stream logs to graylog or papertrail:
SYSLOG_HOST=my.syslog.server,SYSLOG_PORT=514
Performance Tuning
- For 100+ users, switch from SQLite to MariaDB:
services: db: image: mariadb:10 environment: MYSQL_ROOT_PASSWORD: strong_password MYSQL_DATABASE: vaultwarden MYSQL_USER: vaultwarden MYSQL_PASSWORD: another_password vaultwarden: environment: DATABASE_URL=mysql://vaultwarden:another_password@db:3306/vaultwarden - Enable database connection pooling:
DB_CONNECTION_RETIRES=5
Migrating from Bitwarden Cloud to Vaultwarden
- Export data from Bitwarden: Settings → Account → Export → JSON (unencrypted)
- Temporarily enable signups on Vaultwarden: set
SIGNUPS_ALLOWED=truein environment, restart - Create a new empty account on Vaultwarden with the same email as your Bitwarden account
- Use the Vaultwarden CLI or web vault’s import feature: Tools → Import Data → Bitwarden (JSON)
- Delete duplicate organizations if any
- Re-disable signups
- Test login and sync across devices
Updating Vaultwarden
cd /opt/vaultwarden
docker-compose pull vaultwarden
docker-compose up -d
docker image prune -f # remove old images
Check release notes for breaking changes. Subscribe to the GitHub releases RSS feed.
Common Troubleshooting
| Issue | Solution |
|---|---|
| WebSocket not syncing | Verify Nginx WebSocket proxy block; ensure WEBSOCKET_PORT=3012 is set |
| “Invalid domain” error | Set DOMAIN env var exactly as accessed in browser (with trailing slash removed) |
| Cannot send email | Check SMTP logs: docker logs vaultwarden; test with telnet |
| High memory usage | Set DB_CONNECTION_POOL_SIZE=5; reduce PASSWORD_ITERATIONS |
| Admin panel 404 | Ensure ADMIN_TOKEN is set and container restarted; access /admin not /admin/ |