Vaultwarden is a Rust-based reimplementation of the Bitwarden server, API-compatible with all official Bitwarden clients (browser extensions, mobile, desktop, CLI) while running comfortably on a 1 vCPU VPS with 1 GB of RAM. Self-hosting gives you full control over your password vault, removes any third-party dependency for credential storage, and keeps the data inside the EU jurisdiction of your choice. This tutorial walks through a production-grade setup using Docker Compose, an automatic HTTPS reverse proxy (Caddy first, with an Nginx alternative), the admin panel, and a backup strategy that you can actually trust.
What You Will Build
By the end of this guide, you will have:
- A Vaultwarden instance running in Docker with persistent storage
- Automatic HTTPS via Let's Encrypt
- Admin panel access protected by an Argon2 token
- Fail2ban protection against brute-force login attempts
- Encrypted daily backups uploaded to off-server storage
Prerequisites
- A VPS running Debian 12 or Ubuntu 24.04 (we use Debian on our Cloud VPS by default)
- A registered domain with an A/AAAA record pointing at the VPS public IP
- Open TCP ports 80 and 443 in your firewall
- SSH root access
Sizing guidance: for a family or small team (under 25 users), 1 vCPU and 1 GB of RAM is plenty. SQLite is the default backend and handles thousands of vault items without issue.
Step 1: Install Docker and Docker Compose
apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg ufw fail2ban
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
> /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
Verify with docker compose version.
Step 2: Generate an Admin Token
Vaultwarden's admin panel must be protected by an Argon2-hashed token, not a plain string:
docker run --rm -it vaultwarden/server:latest /vaultwarden hash
Enter a strong password (at least 32 characters) twice. The output starts with $argon2id$.... Save the full hash; you will paste it into the environment file shortly. Store the plain password in your existing password manager or a sealed offline note.
Step 3: Create the Directory Structure
mkdir -p /opt/vaultwarden/{data,backups}
mkdir -p /opt/caddy/{data,config}
cd /opt/vaultwarden
Step 4: Write the Compose File
Create /opt/vaultwarden/docker-compose.yml:
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
env_file: .env
volumes:
- ./data:/data
networks:
- web
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/caddy/data:/data
- /opt/caddy/config:/config
networks:
- web
networks:
web:
driver: bridge
Step 5: Write the Environment File
Create /opt/vaultwarden/.env:
DOMAIN=https://vault.example.com
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=true
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...your_full_hash_here...'
SENDS_ALLOWED=true
EMERGENCY_ACCESS_ALLOWED=true
WEB_VAULT_ENABLED=true
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=warn
SMTP_HOST=smtp.example.com
SMTP_FROM=vault@example.com
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USERNAME=vault@example.com
SMTP_PASSWORD=your_smtp_password
The single quotes around ADMIN_TOKEN matter: the Argon2 hash contains $ characters that the shell would otherwise interpret.
Setting SIGNUPS_ALLOWED=false after your initial registration prevents random visitors from creating accounts on your instance. You will create your admin account once, then disable signups.
Step 6: Write the Caddyfile
Create /opt/vaultwarden/Caddyfile:
vault.example.com {
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
}
reverse_proxy /notifications/hub vaultwarden:3012
reverse_proxy vaultwarden:80
}
Caddy obtains and renews Let's Encrypt certificates automatically. No manual certbot setup is required.
Step 7: Start the Stack
cd /opt/vaultwarden
docker compose up -d
docker compose logs -f
Watch for the line confirming Vaultwarden is listening on port 80, and Caddy logs showing successful certificate issuance. If the certificate step fails, the most common cause is the domain not yet resolving to the VPS, or ports 80 and 443 being blocked.
Step 8: First Login and Locking Down
- Visit
https://vault.example.com - Register your first account (this becomes the owner account)
- Verify the email with the link delivered to your inbox
- Edit
.envto setSIGNUPS_ALLOWED=false - Restart:
docker compose restart vaultwarden
To invite additional family or team members, visit https://vault.example.com/admin, authenticate with the plain admin password (Vaultwarden hashes it and compares to the Argon2 token), and use the invite feature.
Nginx Alternative
If you prefer Nginx, replace the Caddy service with:
nginx:
image: nginx:alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
networks:
- web
Use certbot --nginx on the host to obtain certificates, then proxy to vaultwarden:80 and vaultwarden:3012 for the WebSocket endpoint. Caddy is simpler for this exact use case, which is why we lead with it.
Step 9: Firewall Rules
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 443/udp
ufw enable
Step 10: Fail2ban for Vaultwarden
Vaultwarden writes failed authentication attempts to its log. Create /etc/fail2ban/filter.d/vaultwarden.conf:
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
ignoreregex =
Create /etc/fail2ban/jail.d/vaultwarden.conf:
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 86400
findtime = 600
Reload with systemctl reload fail2ban. Five failed logins from the same IP within ten minutes earn a 24-hour ban.
Apply the same pattern to a separate jail for the admin endpoint with a stricter maxretry = 3.
Backup Strategy
Vaultwarden's /data directory contains everything: the SQLite database, attachments, sends, icons, and configuration. Backing it up correctly means using the SQLite online backup API (a file-level copy of an in-use SQLite database can be inconsistent).
Create /opt/vaultwarden/backup.sh:
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/opt/vaultwarden/backups
DATE=$(date +%Y%m%d-%H%M%S)
DATA_DIR=/opt/vaultwarden/data
mkdir -p "$BACKUP_DIR"
# Online SQLite backup
docker exec vaultwarden sqlite3 /data/db.sqlite3 \
".backup '/data/db-backup.sqlite3'"
# Tar the data directory excluding the live SQLite
tar --exclude='db.sqlite3' \
--exclude='db.sqlite3-wal' \
--exclude='db.sqlite3-shm' \
--exclude='icon_cache' \
-czf "$BACKUP_DIR/vw-$DATE.tar.gz" \
-C "$DATA_DIR" .
# Encrypt
gpg --batch --yes --passphrase-file /root/.backup-pass \
--symmetric --cipher-algo AES256 \
-o "$BACKUP_DIR/vw-$DATE.tar.gz.gpg" \
"$BACKUP_DIR/vw-$DATE.tar.gz"
rm "$BACKUP_DIR/vw-$DATE.tar.gz"
# Keep 14 days locally
find "$BACKUP_DIR" -name "vw-*.tar.gz.gpg" -mtime +14 -delete
# Upload to off-site storage (rclone, restic, or rsync)
rclone copy "$BACKUP_DIR/vw-$DATE.tar.gz.gpg" remote:vaultwarden-backups/
Schedule it daily:
chmod +x /opt/vaultwarden/backup.sh
echo "0 3 * * * root /opt/vaultwarden/backup.sh >> /var/log/vw-backup.log 2>&1" \
> /etc/cron.d/vaultwarden-backup
Test restoration to a different host at least once before you trust this system. A backup you have never restored is a guess, not a backup.
Updating Vaultwarden
cd /opt/vaultwarden
docker compose pull
docker compose up -d
Vaultwarden upgrades are usually painless. Watch the release notes for breaking changes to environment variables.
FAQ
Is Vaultwarden compatible with official Bitwarden clients? Yes. The API surface is fully compatible with the official browser extensions, mobile apps, desktop clients, and CLI. Point the client at your self-hosted URL in the settings before login.
How much RAM does Vaultwarden actually use? A typical idle instance uses around 30 to 50 MB of RAM. Even with hundreds of users it rarely exceeds 200 MB.
Can I use MariaDB or PostgreSQL instead of SQLite? Yes, by setting DATABASE_URL in the environment. For most self-hosters, SQLite is faster and simpler, with no operational downside until you reach thousands of active users.
What happens if Vaultwarden goes down? Bitwarden clients keep an encrypted local cache and continue functioning for password retrieval. Sync, new entries, and cross-device updates pause until the server is back. This is one reason a redundant backup matters more than instance redundancy for small deployments.
Do I need the admin panel? You need it for initial setup and occasional administration (inviting users, viewing diagnostics). Once configured, you can disable it by removing ADMIN_TOKEN from .env and restarting.
Where to Host It
Vaultwarden's compute requirements are modest, but the latency between your devices and the server matters for everyday use. An EU-based VPS gives most European users single-digit millisecond response times. Our Cloud VPS runs on Ceph-backed storage with included Proxmox Backup Server, which complements the local backup script above with deduplicated off-cluster snapshots. For a vault you may rely on for years, predictable infrastructure and quick operator response matter more than headline benchmarks.