Self-Hosting Your Own Password Manager: Vaultwarden Setup and Hardening Guide
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
Your password manager is arguably the most sensitive piece of software you use — it holds the keys to everything. Trusting a third-party cloud service with that data has always made me a little uneasy, which is exactly why I moved to a self-hosted Vaultwarden instance two years ago and haven't looked back. Vaultwarden is a community-written, Rust-based server that implements the Bitwarden API, meaning every official Bitwarden client — mobile, browser extension, desktop — works against it perfectly, at a fraction of the resource footprint of the official Bitwarden server.
In this guide I'll walk you through deploying Vaultwarden with Docker Compose, fronting it with Caddy for automatic HTTPS, and then applying the hardening steps I consider non-negotiable before I'd trust it with production credentials. I'm running this on a DigitalOcean Droplet — a $6/month basic Droplet is more than enough — but the same steps apply to any Ubuntu 22.04 or 24.04 VPS or homelab machine.
Prerequisites
Before we start, make sure you have the following in place:
- A VPS or home server running Ubuntu 22.04 LTS or newer
- Docker Engine and Docker Compose v2 installed (
docker composenotdocker-compose) - A domain name with an A record pointing to your server's public IP
- Ports 80 and 443 open in your firewall (UFW:
ufw allow 80/tcp && ufw allow 443/tcp) - A non-root sudo user for day-to-day operations
If you want a solid, affordable place to spin this up, DigitalOcean Droplets give you dependable uptime with a 99.99% SLA and predictable monthly pricing. Their one-click Docker marketplace image is also a quick way to get Docker pre-installed.
Directory Structure and Docker Compose File
I keep all my self-hosted services under /opt/services/, one directory per app. Create the Vaultwarden directory and the Caddy configuration directories now:
sudo mkdir -p /opt/services/vaultwarden/vw-data
sudo mkdir -p /opt/services/caddy/{config,data}
cd /opt/services/vaultwarden
Now create the docker-compose.yml. I use a single Compose file that defines both the Vaultwarden container and a Caddy reverse proxy container, joined on a shared internal network. Nothing in Vaultwarden is exposed directly to the internet — all traffic must flow through Caddy.
cat > /opt/services/vaultwarden/docker-compose.yml <<'EOF'
version: "3.9"
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.yourdomain.com"
SIGNUPS_ALLOWED: "false"
INVITATIONS_ALLOWED: "false"
ADMIN_TOKEN_FILE: "/run/secrets/vw_admin_token"
LOG_LEVEL: "warn"
SMTP_HOST: "" # fill in if you want email 2FA / invites
SMTP_FROM: ""
SMTP_PORT: "587"
SMTP_SECURITY: "starttls"
SMTP_USERNAME: ""
SMTP_PASSWORD: ""
volumes:
- ./vw-data:/data
- ./secrets/vw_admin_token:/run/secrets/vw_admin_token:ro
networks:
- internal
caddy:
image: caddy:2-alpine
container_name: caddy_vw
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/services/caddy/data:/data
- /opt/services/caddy/config:/config
networks:
- internal
networks:
internal:
driver: bridge
internal: false
EOF
SIGNUPS_ALLOWED is set to "false" from the very first boot. If you leave it true, anyone who finds your URL can create an account. Create your own account immediately on first startup, then restart with signups disabled — or use the admin panel to invite specific email addresses only.Creating the Admin Token Secret
Vaultwarden has an /admin panel that's protected by a single token. Instead of baking it into an environment variable in plain text (bad), I'm mounting it as a file secret. Generate an argon2 hash of a strong passphrase:
# Install argon2 if needed
sudo apt install -y argon2
# Generate a hashed token — use your own strong passphrase
echo -n "your-very-long-admin-passphrase" | argon2 "$(openssl rand -base64 16)" -e -id -k 65540 -t 3 -p 4
# Create the secrets directory and write the hash
mkdir -p /opt/services/vaultwarden/secrets
echo '$argon2id$v=19$m=65540,t=3,p=4$...' \
> /opt/services/vaultwarden/secrets/vw_admin_token
chmod 600 /opt/services/vaultwarden/secrets/vw_admin_token
Replace the echo'd string with the actual argon2 output from the command above. Vaultwarden 1.30+ understands argon2 hashed tokens natively — you no longer need to store a raw string.
Configuring Caddy as the Reverse Proxy
I prefer Caddy over Nginx Proxy Manager for this use case because it handles Let's Encrypt certificates fully automatically — no certbot cron jobs, no manual renewal. The Caddyfile is also beautifully concise:
cat > /opt/services/vaultwarden/Caddyfile <<'EOF'
vault.yourdomain.com {
encode zstd gzip
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
-Server
}
# Block direct admin access from the public internet
# Comment this out to manage via browser when needed, then re-enable
@admin_block {
path /admin*
not remote_ip 127.0.0.1/8 ::1/128
}
respond @admin_block "Forbidden" 403
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
EOF
@admin_block matcher in the Caddyfile above blocks /admin access from any IP that isn't localhost. When I need to access the admin panel, I open an SSH tunnel first: ssh -L 8080:localhost:443 user@yourserver, then browse to https://vault.yourdomain.com/admin via the tunnel. This keeps the admin panel completely off the public internet without a separate VPN.First Boot and Account Creation
Before starting, temporarily set SIGNUPS_ALLOWED: "true" in your Compose file just for the first boot. Then:
cd /opt/services/vaultwarden
docker compose up -d
# Watch logs to confirm healthy startup
docker compose logs -f vaultwarden
Open https://vault.yourdomain.com in your browser. Caddy will automatically obtain a Let's Encrypt certificate — this usually takes under 30 seconds. Create your account, verify it works, then edit the Compose file to set SIGNUPS_ALLOWED: "false" and restart:
docker compose up -d --force-recreate vaultwarden
Hardening: Fail2ban for Vaultwarden
Vaultwarden writes failed login attempts to its log file. I configure fail2ban to parse those logs and ban repeat offenders. First, install fail2ban:
sudo apt install -y fail2ban
# Create a Vaultwarden filter
sudo tee /etc/fail2ban/filter.d/vaultwarden.conf <<'EOF'
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =
EOF
# Create the jail
sudo tee /etc/fail2ban/jail.d/vaultwarden.conf <<'EOF'
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/services/vaultwarden/vw-data/vaultwarden.log
maxretry = 5
bantime = 1800
findtime = 300
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden
Backup Strategy
All of Vaultwarden's data — the SQLite database, attachments, and sends — lives in the ./vw-data directory. Vaultwarden also supports automatic SQLite WAL-mode backups. I run a nightly script that stops the container briefly, copies the data directory to an offsite location, then restarts:
#!/bin/bash
# /opt/scripts/backup-vaultwarden.sh
set -euo pipefail
COMPOSE_DIR="/opt/services/vaultwarden"
BACKUP_DIR="/mnt/backups/vaultwarden"
DATE=$(date +%Y-%m-%d)
mkdir -p "$BACKUP_DIR"
cd "$COMPOSE_DIR"
docker compose exec vaultwarden sqlite3 /data/db.sqlite3 ".backup /data/db-backup.sqlite3"
tar -czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz" ./vw-data
# Keep 14 days of backups
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +14 -delete
echo "Vaultwarden backup complete: $DATE"
Add it to cron: sudo crontab -e and add 0 3 * * * /opt/scripts/backup-vaultwarden.sh. I also push these backups to a DigitalOcean Space (S3-compatible object storage) using rclone, giving me an offsite copy for roughly $0.02/month at the tiny sizes involved.
Additional Hardening Checklist
- Enable 2FA on your Vaultwarden account — TOTP works out of the box. Duo and Yubikey require a paid Bitwarden subscription but work fine with Vaultwarden's own implementation.
- Disable the WebSocket notifications port (3012) if you don't need live sync — remove it from any published ports.
- Set
DISABLE_ICON_DOWNLOAD: "true"in environments where you don't want the server fetching external favicons, which can leak your server's IP. - Keep the image updated —
docker compose pull && docker compose up -donce a week, or automate with Watchtower targeting only this container. - Restrict UFW so only ports 80, 443, and your SSH port are open. Vaultwarden's internal port 80 is never exposed directly.
Connecting Your Bitwarden Clients
In any official Bitwarden client, look for the "Self-hosted environment" or server URL setting before logging in. Enter https://vault.yourdomain.com as the server URL. Everything else — login, 2FA, vault sync, browser extension — works exactly as it does with bitwarden.com. Your family members can use the same server; just invite them via the admin panel rather than enabling open registration.
Wrapping Up
At this point you have a fully functional, hardened Vaultwarden instance: TLS-terminated by Caddy with strict security headers, admin panel locked behind an SSH tunnel, fail2ban watching for brute-force attempts, and nightly backups running automatically. The total RAM footprint sits at around 15–20 MB — Vaultwarden in Rust is genuinely tiny. I run mine alongside a dozen other services on a single $6 Droplet and barely notice it's there.
Your next steps: set up SMTP so Vaultwarden can send emergency access notifications and 2FA recovery emails, and consider fronting the whole server with a Cloudflare Tunnel if you'd prefer not to expose your server's real IP at all. Both topics have dedicated tutorials here on CompactHost.
If you don't yet have a VPS to host this on, create your DigitalOcean account today and get started — a Basic Droplet is all you need for a personal Vaultwarden instance.
Discussion