Why Self-Host Your Password Manager?
Password managers are arguably the single most important security tool you can adopt — but trusting every credential you own to a third-party cloud service is a genuine leap of faith. Breaches happen, services get acquired, pricing models change, and your vault is along for the ride every time. If you’re already running a homelab, hosting your own password manager is a logical extension: you get the same polished Bitwarden apps on every device (mobile, browser extension, desktop) with complete control over where the data actually lives.
Enter Vaultwarden — a lightweight, Bitwarden-compatible server written in Rust that runs as a single Docker container. It supports organizations, collections, emergency access, TOTP, WebAuthn hardware keys, and the full Bitwarden send feature — without the multi-container complexity of the official Bitwarden server. On a modest homelab machine it idles around 10–20 MB of RAM. It’s one of those self-hosted apps that simply disappears into the background once it’s running.
In this guide we’ll spin up Vaultwarden with Docker Compose, front it with a Caddy reverse proxy for automatic HTTPS, configure the admin panel, set up SMTP, connect all the Bitwarden clients, and put a solid backup and update routine in place.
Prerequisites
You’ll need:
- A Linux host with Docker and Docker Compose v2 installed — if you’re starting fresh, our Docker beginner’s guide covers the full installation process
- A domain name or subdomain with an A/AAAA record pointing at your server — Caddy uses it to pull a Let’s Encrypt certificate automatically
- Ports 80 and 443 open on your firewall and forwarded on your router (if you’re hosting at home)
- Basic comfort with the command line and editing YAML files
If you’d rather keep Vaultwarden off the public internet entirely, you can pair it with Tailscale to access your vault from anywhere over a private WireGuard mesh — no port forwarding, no public exposure. We cover that setup at the end.
Directory Structure
Create a clean working directory with the subdirectories Vaultwarden and Caddy will use:
mkdir -p ~/vaultwarden/{data,caddy/data,caddy/config}
cd ~/vaultwarden
The layout looks like this:
vaultwarden/
├── docker-compose.yml
├── Caddyfile
├── data/ # Vaultwarden SQLite DB, attachments, icon cache
├── caddy/
│ ├── data/ # Caddy TLS certificates (persisted across restarts)
│ └── config/ # Caddy runtime config
Generating the Admin Token
Before writing the Compose file, generate the admin token. Vaultwarden 1.30+ supports Argon2-hashed tokens, which is strongly preferred over plain text:
# Pull the image first (needed to run the hash subcommand)
docker pull vaultwarden/server:latest
# Generate an Argon2 hash of your chosen admin password
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp
You’ll be prompted to enter a password. The output will look like:
Password: ************
ADMIN_TOKEN: $argon2id$v=19$m=65540,t=3,p=4$bXlzYWx0bXlzYWx0bXlzYWx0$...
Copy the full $argon2id$... string — you’ll use it in a moment. Keep the plain-text password somewhere safe; you’ll need it to log into the admin panel (you type the password, not the hash).
Docker Compose Configuration
Create the Compose file, substituting your domain and your generated admin token:
cat > docker-compose.yml <<'EOF'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.yourdomain.com"
SIGNUPS_ALLOWED: "true" # flip to false after first account
ADMIN_TOKEN: '$argon2id$v=19$...' # paste your full hash here
WEBSOCKET_ENABLED: "true"
SMTP_HOST: "smtp.yourprovider.com"
SMTP_FROM: "vault@yourdomain.com"
SMTP_FROM_NAME: "Vaultwarden"
SMTP_SECURITY: "starttls"
SMTP_PORT: "587"
SMTP_USERNAME: "vault@yourdomain.com"
SMTP_PASSWORD: "your-smtp-password"
LOG_LEVEL: "warn"
volumes:
- ./data:/data
networks:
- proxy
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
- ./caddy/data:/data
- ./caddy/config:/config
networks:
- proxy
networks:
proxy:
driver: bridge
EOF
Key environment variables explained:
- DOMAIN: Must exactly match the URL clients will use — Vaultwarden embeds this in WebAuthn credentials and push notification URLs
- SIGNUPS_ALLOWED: Set to
trueonly long enough to create your account, then flip tofalseand redeploy - ADMIN_TOKEN: The full Argon2 hash wrapped in single quotes — single quotes prevent the shell from interpreting the dollar signs
- WEBSOCKET_ENABLED: Enables real-time vault sync; clients see changes from other devices within a second without polling
- SMTP_*: Required if you want email verification, emergency access, and 2FA backup codes delivered via email — not strictly mandatory but strongly recommended
Caddy Reverse Proxy Configuration
Caddy handles TLS certificate issuance and renewal automatically via Let’s Encrypt or ZeroSSL — no certbot, no cron jobs, no manual renewal headaches. Create the Caddyfile:
cat > Caddyfile <<'EOF'
vault.yourdomain.com {
encode zstd gzip
# WebSocket notifications go directly to Vaultwarden's WS port
reverse_proxy /notifications/hub vaultwarden:3012
# All other traffic goes to the main HTTP port
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
# Hardened security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
-Server
}
}
EOF
Replace vault.yourdomain.com with your actual domain in both the Caddyfile and the Compose file. DNS must be resolving before you start — Caddy contacts Let’s Encrypt on first launch and the ACME challenge fails if the domain isn’t live yet.
First Launch and Initial Setup
Start the stack:
docker compose up -d
Watch for TLS certificate issuance in the Caddy logs:
docker compose logs -f caddy
# Look for:
# {"level":"info","msg":"certificate obtained successfully","domain":"vault.yourdomain.com"}
Then confirm Vaultwarden is listening:
docker compose logs -f vaultwarden
# Look for:
# [INFO] Rocket has launched from http://0.0.0.0:80
# [INFO] Listening for websocket on 0.0.0.0:3012
Navigate to https://vault.yourdomain.com/admin and log in with your plain-text admin password (not the hash). From here you can:
- Send yourself an invitation email (necessary once signups are disabled)
- Verify your SMTP configuration with the Test SMTP button
- Configure 2FA policy — enforce it for all accounts under Settings
- Set attachment size limits and enabled features
- Review connected users and delete accounts if needed
Once you’re satisfied, go to https://vault.yourdomain.com, create your account, then immediately set SIGNUPS_ALLOWED: "false" in the Compose file and redeploy:
docker compose up -d
Connecting the Bitwarden Clients
Vaultwarden’s biggest advantage over other self-hosted password solutions is full compatibility with the official Bitwarden apps — no forks, no sketchy APKs.
Browser Extension (Chrome, Firefox, Edge, Safari)
- Install the official Bitwarden extension from your browser’s store
- Click the extension icon, then the gear icon at the top left
- Toggle Self-hosted environment
- Enter your server URL:
https://vault.yourdomain.com - Save and log in with your Vaultwarden credentials
Mobile (iOS and Android)
- Open the Bitwarden app
- On the login screen, tap the region selector (shows “bitwarden.com” by default)
- Tap Self-hosted and enter your server URL
- Log in normally
Desktop App
- Open Bitwarden → click the region selector in the top-left corner
- Choose Self-hosted and enter your server URL
- Save and log in
Once connected, WebSocket sync kicks in immediately — edit an item on your phone and the browser extension reflects the change in under a second.
Organizations and Shared Collections
Vaultwarden fully supports Bitwarden Organizations, which let you share credentials with family members or a small team without giving anyone else your master password. Each shared item lives in a Collection that you control:
# In the admin panel:
# Users > Invite a user by email (when signups are disabled, this is required)
# Then in the Bitwarden app:
# Organizations > Create Organization > invite users and assign Collection access
Organizations on self-hosted Vaultwarden are free and unlimited — the official cloud Bitwarden charges for this feature. It makes Vaultwarden an excellent choice for families or small teams that want a shared vault without a monthly subscription.
Backup Strategy
Your entire vault lives in ./data/. Vaultwarden uses SQLite by default, so backups need to create a consistent snapshot even while the database is active. The correct approach uses SQLite’s online backup via the sqlite3 CLI tool:
#!/bin/bash
# /opt/scripts/backup-vaultwarden.sh
VAULTWARDEN_DATA="$HOME/vaultwarden/data"
BACKUP_DIR="/mnt/nas/vaultwarden-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/vaultwarden_${TIMESTAMP}.tar.gz"
# Create a WAL-safe consistent copy of the SQLite DB
sqlite3 "${VAULTWARDEN_DATA}/db.sqlite3" \
".backup '${VAULTWARDEN_DATA}/db_backup_${TIMESTAMP}.sqlite3'"
# Archive the data directory (DB backup, attachments, icon cache, config)
tar -czf "$BACKUP_FILE" \
-C "$(dirname "${VAULTWARDEN_DATA}")" \
"$(basename "${VAULTWARDEN_DATA}")"
# Remove the temporary SQLite copy
rm -f "${VAULTWARDEN_DATA}/db_backup_${TIMESTAMP}.sqlite3"
# Retain only the last 30 backups
ls -tp "${BACKUP_DIR}"/vaultwarden_*.tar.gz | \
tail -n +31 | xargs -r rm --
echo "Backup complete: $BACKUP_FILE ($(du -sh "$BACKUP_FILE" | cut -f1))"
Make it executable and schedule it in cron:
chmod +x /opt/scripts/backup-vaultwarden.sh
crontab -e
# Add this line:
0 2 * * * /opt/scripts/backup-vaultwarden.sh >> /var/log/vaultwarden-backup.log 2>&1
sqlite3 is typically available on most Linux distributions — install it with apt install sqlite3 if it’s missing. The .backup dot-command performs an online copy that’s safe to run without stopping the container.
If you’re already managing your homelab with Ansible, you can plug this into a centralized Ansible playbook that orchestrates backups across all your services and ships archives to a remote target like a NAS or an S3-compatible bucket.
Keeping Vaultwarden Updated
Vaultwarden releases new versions frequently — often within days of new Bitwarden client releases to maintain compatibility. The update process is straightforward:
# Pull the latest image
docker compose pull vaultwarden
# Recreate the container with the new image
docker compose up -d --no-build
# Verify the new version is running
docker exec vaultwarden /vaultwarden --version
If you want to pin to a specific version (good practice for production stability), replace latest in the Compose file with a version tag like vaultwarden/server:1.31.0. Check the Vaultwarden releases page for changelogs before upgrading major versions.
Security Hardening Checklist
Vaultwarden holds every password you own. Treat it accordingly:
- Disable signups immediately after creating your account — set
SIGNUPS_ALLOWED: "false"and redeploy - Enable 2FA on your vault account — TOTP apps (Aegis on Android, Raivo on iOS) or a hardware key via FIDO2/WebAuthn
- Use an Argon2-hashed admin token, not a plain-text string — the
vaultwarden hashcommand above handles this - Restrict admin panel access by IP — in the Caddyfile, add
@admin { path /admin* }and restrict withhandle @admin { remote_ip 192.168.1.0/24 }to allow only LAN access - Enable fail2ban on the host to watch container logs for failed logins and block offending IPs
- Keep images updated — Vaultwarden patches security issues promptly; run the update procedure above at least monthly
- Test your backups — restore the SQLite backup into a temporary container and verify you can log in; untested backups aren’t backups
- Keep an emergency export — export an encrypted Bitwarden JSON backup and store it offline; if your server goes down you’re not locked out
Switching from SQLite to PostgreSQL
For most personal and small-team setups, SQLite is perfectly adequate — Vaultwarden’s workload is light and the database rarely exceeds a few megabytes. But if you’re running Vaultwarden for a larger organization or experiencing occasional write contention warnings in the logs, migrating to PostgreSQL is straightforward.
Add a Postgres service to your Compose file:
postgres:
image: postgres:16-alpine
container_name: vaultwarden-db
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vwuser
POSTGRES_PASSWORD: strongpasswordhere
volumes:
- ./pgdata:/var/lib/postgresql/data
networks:
- proxy
Then update the Vaultwarden service to use it:
DATABASE_URL: "postgresql://vwuser:strongpasswordhere@postgres/vaultwarden"
Vaultwarden handles schema migrations automatically on startup — no manual SQL required. Use the official migration guide in the Vaultwarden wiki when moving an existing SQLite vault to Postgres, since there’s a specific import order to follow to avoid foreign key violations.
Going Off-Grid with Tailscale
If you’d rather not expose Vaultwarden to the internet at all, combine it with Tailscale for a completely private setup. Enable Tailscale’s HTTPS certificates on your node (tailscale cert myhomelab.tail12345.ts.net), set DOMAIN to your MagicDNS hostname, and point Caddy at Tailscale’s local cert files instead of using Let’s Encrypt. The Bitwarden clients connect from any device with the Tailscale app active — over a WireGuard tunnel — with no ports open and no attack surface exposed to the internet.
This is the setup we run for personal vaults. The service is completely invisible to the internet while still accessible from phones, laptops, and tablets anywhere in the world. It’s the best of both worlds.
Wrapping Up
Vaultwarden is one of the cleanest self-hosted apps in the homelab ecosystem: minimal resource footprint, excellent client compatibility, active development, and a feature set that rivals or beats the official cloud offering for personal use. Once it’s running and backed up, you’ll forget it’s there — which is exactly what you want from infrastructure.
If you’re building out a broader self-hosting stack, our Nextcloud installation guide is the natural next step — a full Google Drive, Calendar, and Contacts replacement that pairs well with a self-hosted password manager. Between the two, you can de-Google a significant chunk of your digital life while keeping everything on hardware you control.