Deploying Self-Hosted Password Managers with Docker
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
I got tired of trusting password managers to cloud-hosted SaaS services a long time ago. When I discovered Vaultwarden—a lightweight, self-hosted Bitwarden-compatible server—my password security strategy changed completely. In this guide, I'll show you exactly how I deployed a production-ready password manager using Docker, complete with SSL encryption, automated backups, and hardened access controls.
You can run this on your homelab, a VPS (like the $40/year deals from RackNerd), or even a Raspberry Pi. The entire stack fits in under 500MB of RAM.
Why Self-Host Your Password Manager?
Cloud password managers are convenient, but they represent a single point of failure. If their server gets breached, every password you've entrusted to them is exposed. When you self-host, only you control the infrastructure, the backups, and the encryption keys.
Vaultwarden is a Rust implementation of Bitwarden's API, compatible with all official Bitwarden clients (Chrome extension, mobile apps, web vault). You get the same UX you know, but running on hardware you own. The performance is snappy, the resource footprint is tiny, and the community support is exceptional.
I prefer Vaultwarden over building something from scratch because it's battle-tested, actively maintained, and saves me 20+ hours of development work. The trade-off between convenience and control is perfectly balanced.
Prerequisites
Before we begin, you'll need:
- A Linux server (Ubuntu 22.04 LTS recommended) or VPS. I run mine on RackNerd's budget VPS—about $40/year for 1 vCore, 1GB RAM, and 20GB SSD. That's more than enough.
- Docker and Docker Compose installed
- A domain name and ability to set DNS records
- Basic familiarity with the command line
Docker Compose Setup for Vaultwarden
I manage Vaultwarden using Docker Compose because it handles networking, volumes, and environment variables cleanly. Here's the production-ready configuration I use:
version: '3.8'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: always
ports:
- "8000:80"
environment:
DOMAIN: https://vault.example.com
SIGNUPS_ALLOWED: false
INVITATIONS_ORG_ALLOW_USER: true
LOG_LEVEL: info
EXTENDED_LOGGING: true
EXTENDED_LOGGING_FORMAT: json
SHOW_PASSWORD_HINT: false
DISABLE_2FA_REMEMBER: true
ROCKET_ADDRESS: 0.0.0.0
ROCKET_PORT: 80
DATABASE_URL: postgresql://vaultwarden:${POSTGRES_PASSWORD}@postgres:5432/vaultwarden
ADMIN_TOKEN: ${ADMIN_TOKEN}
volumes:
- ./vw-data:/data
networks:
- vaultwarden-network
depends_on:
- postgres
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/alive"]
interval: 30s
timeout: 3s
retries: 3
postgres:
image: postgres:15-alpine
container_name: vaultwarden-db
restart: always
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres-data:/var/lib/postgresql/data
networks:
- vaultwarden-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vaultwarden"]
interval: 10s
timeout: 3s
retries: 3
networks:
vaultwarden-network:
driver: bridge
volumes:
vw-data:
postgres-data:
Save this as docker-compose.yml. Next, create a .env file in the same directory:
POSTGRES_PASSWORD=your-super-secure-random-password-here
ADMIN_TOKEN=$(openssl rand -base64 32)
Generate a strong admin token by running this before deploying:
openssl rand -base64 32
I set SIGNUPS_ALLOWED: false because I want only invited users on my vault. The ADMIN_TOKEN protects the admin panel, and the PostgreSQL backend is more reliable than SQLite for production use.
.env file to version control. I add it to .gitignore immediately. Store your `ADMIN_TOKEN` and `POSTGRES_PASSWORD` in a secure location—write them down on paper if you have to. If you lose them, you'll need to reset the entire database.Deploying Behind Caddy (or Nginx)
Vaultwarden listens on port 8000 in the compose file, but you absolutely must proxy it through a reverse proxy with HTTPS. I use Caddy because it handles SSL certificates automatically via Let's Encrypt.
Add Caddy to your Docker Compose stack:
caddy:
image: caddy:latest
container_name: caddy-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
networks:
- vaultwarden-network
depends_on:
- vaultwarden
volumes:
caddy-data:
caddy-config:
Create a Caddyfile in the same directory:
vault.example.com {
reverse_proxy vaultwarden:80 {
header_uri -Authorization
}
# Rate limiting to protect admin panel
@admin /admin*
rate_limit @admin 10r/m
# Security headers
header Strict-Transport-Security "max-age=31536000; includeSubDomains"
header X-Frame-Options DENY
header X-Content-Type-Options nosniff
header X-XSS-Protection "1; mode=block"
header Referrer-Policy "strict-origin-when-cross-origin"
header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
}
Replace vault.example.com with your actual domain. Caddy will automatically provision and renew your SSL certificate. The security headers I've added protect against common web attacks—I didn't invent these, they're security best practices for password managers.
Launching the Stack
First, create the data directories:
mkdir -p vw-data postgres-data
chmod 700 postgres-data
Then bring everything online:
docker-compose up -d
Check the logs to ensure everything started cleanly:
docker-compose logs -f vaultwarden
You should see messages like "Rocket is ignited. Everybody, buckle up!" which means Vaultwarden is running. If you see database connection errors, wait 10 seconds—PostgreSQL might still be initializing.
Access your vault at https://vault.example.com (assuming DNS is pointing to your server). You'll see the Bitwarden login page. Create your account, and you're ready to start storing passwords.
Securing Your Password Manager
A password manager is only as secure as its network perimeter. I implemented several hardening measures:
Firewall Rules
On the server itself, I use UFW to restrict traffic:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH only from home IP
sudo ufw allow 80/tcp # HTTP (Caddy redirects to HTTPS)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
If you're on a VPS, restrict SSH to your home IP address. This prevents brute-force attacks. Replace YOUR_HOME_IP with your actual IP:
sudo ufw allow from YOUR_HOME_IP to any port 22
Fail2Ban Protection
Install fail2ban to automatically ban IPs attempting to brute-force the admin panel:
sudo apt-get install fail2ban
sudo systemctl enable fail2ban
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
[caddy-auth]
enabled = true
port = http,https
filter = caddy-auth
logpath = /var/log/caddy/*.log
maxretry = 5
Database Backups
Your password vault is only valuable if you can recover it. I back up PostgreSQL daily to cold storage:
#!/bin/bash
BACKUP_DIR="/backups/vaultwarden"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
docker-compose exec -T postgres pg_dump -U vaultwarden vaultwarden | \
gzip > $BACKUP_DIR/vaultwarden_$DATE.sql.gz
# Keep only the last 30 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete
echo "Backup completed: $BACKUP_DIR/vaultwarden_$DATE.sql.gz"
Save this as backup.sh, make it executable, and add it to crontab to run daily:
chmod +x backup.sh
crontab -e
# Add: 0 2 * * * /path/to/backup.sh
Using Vaultwarden as Your Daily Driver
Once deployed, I access my vault through the web interface or the official Bitwarden apps (available for Chrome, Firefox, Edge, iOS, and Android). The native clients sync automatically and work offline, which is a huge convenience factor.
I store more than just passwords: API keys, SSH private keys, secure notes, identity information—anything that needs encryption and access control. The browser extension autofills login forms just like cloud-based services, but I know exactly where my data lives.
For my family, I created separate organization vaults within Vaultwarden, allowing them to share emergency contacts and shared credentials without seeing each other's personal passwords.
Monitoring and Maintenance
Set up basic monitoring to catch issues early. I check the admin panel weekly and review the application logs:
docker-compose logs vaultwarden --tail 100
Update Vaultwarden monthly by pulling the latest image:
docker-compose pull
docker-compose up -d --remove-orphans
This causes zero downtime thanks to the health checks I configured in the compose file.
Cost Comparison
Bitwarden Premium costs $10/month ($120/year). Vaultwarden on a RackNerd VPS costs around $40/year. Even if you run it on old homelab hardware, the electricity is negligible. You break even after four months and own the entire stack forever.
Next Steps
You now have a production-grade password manager running in Docker. I recommend:
- Install the Bitwarden extension in your browser and migrate your existing passwords
- Enable two-factor authentication on your vault account (TOTP supported)
- Set up automated backups before you fill it with sensitive data
- Consider pairing this with a reverse proxy like Traefik or Nginx Proxy Manager if you're running multiple self-hosted services
Your passwords are now under your control. Sleep better knowing they're encrypted, backed up, and accessible only to you.