Self-Hosted Password Manager with Bitwarden on Docker
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
I stopped trusting cloud password managers the moment I realized I couldn't audit their code in production. Bitwarden changed that. When I deployed a self-hosted Bitwarden instance on a $40/year VPS from RackNerd, I gained full encryption, zero vendor lock-in, and the peace of mind that comes with owning my secrets. This guide walks you through the entire process—from docker-compose to SSL to disaster recovery.
Why Self-Host Bitwarden Instead of Commercial Options?
Commercial password managers—1Password, LastPass, Dashlane—charge $2–5 per month. Over five years, that's $120–300 per person. A self-hosted Bitwarden instance costs less than $50/year on a shared VPS. But cost isn't the only reason I recommend it.
When you host Bitwarden yourself, you control the encryption keys. Your vault is encrypted client-side with a master password; the server never sees plaintext credentials. You can audit the code, control backups, and move instances without contacting support. If cloud providers get compromised—and they do—you're not exposed because your data was encrypted before it left your device.
The trade-off? You're responsible for uptime, backups, and security patches. For me, that's a worthwhile exchange. I spend 30 minutes monthly on maintenance; I gain total control.
Choosing Your Infrastructure
You have three deployment options: a Docker-enabled home server, a cheap VPS, or a managed container platform like Railway or Fly.io.
I prefer a VPS because my home internet isn't always reliable, and VPS providers handle DDoS. RackNerd's annual plans hover around $40–50 for a single-core instance with 2GB RAM—enough for Bitwarden and a reverse proxy. Hetzner is more expensive but better for high-traffic setups. For a home server with stable power, Docker Compose on a Raspberry Pi 4 works fine, though you'll need dynamic DNS.
Budget roughly:
- Home server: Electricity cost only (~$5/year)
- Budget VPS (RackNerd/RackNerd NewYear): $40–60/year
- Mid-tier VPS (Hetzner): $4–5/month (~$50–60/year)
- Managed platform: $5–10/month
Setting Up Bitwarden with Docker Compose
I'll assume you have a VPS with Docker and Docker Compose installed. If not, run this on a fresh Ubuntu 22.04 system:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Create a project directory and docker-compose.yml:
mkdir -p /opt/bitwarden && cd /opt/bitwarden
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
bitwarden:
image: vaultwarden/server:latest
container_name: bitwarden
restart: always
ports:
- "127.0.0.1:80:80"
environment:
DOMAIN: https://passwords.example.com
SIGNUPS_ALLOWED: false
INVITATIONS_ORG_ALLOW_USER: true
ROCKET_ADDRESS: 0.0.0.0
ROCKET_PORT: 80
LOG_FILE: /data/vaultwarden.log
DATA_LIMIT: 536870912
EXTENDED_LOGGING: true
EXTENDED_LOGGING_FORMAT: "{start_time} | {method} {uri} | {status} | {ip}"
ADMIN_TOKEN: ${ADMIN_TOKEN}
SHOW_PASSWORD_HINT: false
PASSWORD_HINT_ON: false
volumes:
- ./vw-data:/data
networks:
- bitwarden-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/alive"]
interval: 30s
timeout: 3s
retries: 3
start_period: 30s
caddy:
image: caddy:latest
container_name: bitwarden-caddy
restart: always
ports:
- "80:80"
- "443:443"
environment:
DOMAIN: passwords.example.com
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- bitwarden-net
depends_on:
- bitwarden
volumes:
caddy_data:
caddy_config:
networks:
bitwarden-net:
driver: bridge
EOF
Now create the Caddyfile for HTTPS termination. Caddy handles Let's Encrypt automatically:
cat > Caddyfile << 'EOF'
passwords.example.com {
reverse_proxy http://bitwarden:80 {
header_up X-Real-IP {http.request.remote.host}
header_up X-Forwarded-For {http.request.remote.host}
header_up X-Forwarded-Proto {http.request.proto}
header_up X-Forwarded-Host {http.request.host}
}
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "no-referrer"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
encode gzip
file_server disable
}
EOF
passwords.example.com with your actual domain in both files. Your domain must have an A record pointing to your server's IP before starting Caddy, or certificate issuance will fail. Also, generate a strong ADMIN_TOKEN (use openssl rand -hex 32) and set it in your environment or a .env file—never commit credentials to version control.Create a .env file to store sensitive values:
cat > .env << 'EOF'
ADMIN_TOKEN=$(openssl rand -hex 32)
EOF
chmod 600 .env
Start the services:
docker-compose up -d
docker-compose logs -f bitwarden
Wait 30–60 seconds for Caddy to negotiate a certificate. Visit https://passwords.example.com and you should see the Bitwarden login page with a valid SSL cert (green lock in your browser).
Initial Configuration and Security
First login: go to https://passwords.example.com/admin and log in with your ADMIN_TOKEN. Inside the admin panel, disable invitations if you're solo, enable email verification, and set a password hint policy.
Create your main account by signing up (since we left SIGNUPS_ALLOWED: false, this only works if it's your first account). Then immediately disable signups in the admin panel to prevent brute-force registration attacks.
I recommend these additional security settings in the Bitwarden admin UI:
- Enable "Require 2FA for all admins"
- Set "User Org Invite Expiration" to 7 days
- Disable "Allow deletion of users"
- Enable "Account takeover notification"
For extra paranoia, add fail2ban to your VPS to block brute-force attempts on SSH and the Bitwarden login:
sudo apt-get install -y fail2ban
sudo systemctl enable fail2ban
# Create a filter for Bitwarden
sudo tee /etc/fail2ban/filter.d/bitwarden.conf > /dev/null << 'EOF'
[Definition]
failregex = ^.* "POST /identity/connect/token HTTP/1.1" 401
^.* Invalid Username or Password\.
ignoreregex =
EOF
# Create a jail
sudo tee /etc/fail2ban/jail.d/bitwarden.conf > /dev/null << 'EOF'
[bitwarden]
enabled = true
port = http,https
filter = bitwarden
logpath = /var/lib/docker/containers/*/bitwarden-*-json.log
maxretry = 5
findtime = 600
bantime = 3600
EOF
sudo systemctl restart fail2ban
Backup Strategy (Critical)
Your password vault is worthless if you lose it. I back up Bitwarden weekly to encrypted S3 and monthly to a USB drive in my safe deposit box.
Create a backup script:
cat > /opt/bitwarden/backup.sh << 'EOF'
#!/bin/bash
set -e
BACKUP_DIR="/opt/bitwarden/backups"
DATE=$(date +%Y%m%d_%H%M%S)
ARCHIVE="bitwarden_backup_$DATE.tar.gz"
mkdir -p "$BACKUP_DIR"
# Stop the container to ensure data consistency
docker-compose -f /opt/bitwarden/docker-compose.yml pause bitwarden
# Create tarball
tar -czf "$BACKUP_DIR/$ARCHIVE" \
/opt/bitwarden/vw-data \
/opt/bitwarden/Caddyfile \
/opt/bitwarden/docker-compose.yml
# Resume container
docker-compose -f /opt/bitwarden/docker-compose.yml unpause bitwarden
# Optional: upload to S3
# aws s3 cp "$BACKUP_DIR/$ARCHIVE" s3://my-backups/bitwarden/
# Keep only the last 30 days of backups locally
find "$BACKUP_DIR" -name "bitwarden_backup_*.tar.gz" -mtime +30 -delete
echo "Backup completed: $ARCHIVE"
EOF
chmod +x /opt/bitwarden/backup.sh
Schedule it with cron. Edit your crontab:
crontab -e
# Add this line to run backups every Sunday at 2 AM
0 2 * * 0 /opt/bitwarden/backup.sh
Client Setup Across Devices
The Bitwarden browser extension and mobile app are free and connect to any Bitwarden server, including self-hosted instances.
In the browser extension (or mobile app), go to Settings → Server URL and enter https://passwords.example.com. Log in with your master password. Sync happens transparently; credentials are encrypted/decrypted locally.
I use Bitwarden on:
- Chrome, Firefox, Safari (browser extensions)
- iOS and Android (official apps)
- macOS and Windows (desktop apps)
All devices pull from the same encrypted vault. If you add a password on your phone, it's available on your laptop within seconds.
Monitoring and Maintenance
Check logs weekly to catch any errors early:
docker-compose -f /opt/bitwarden/docker-compose.yml logs --tail=100 bitwarden
Look for failed login attempts, permission errors, or database issues. The health check I included in the docker-compose file ensures the container restarts if it becomes unresponsive.
Keep Docker images updated monthly:
cd /opt/bitwarden
docker-compose pull
docker-compose up -d
Monitor disk usage. Vaultwarden typically uses less than 50MB for small vaults, but with database bloat over years, you might hit 200MB. Check with:
du -sh /opt/bitwarden/vw-data
Cost Comparison: Self-Hosted vs. Cloud
Let me break down the five-year math:
- 1Password: $2.99/month × 12 × 5 = $179 (plus potential price increases)
- Dashlane: $3.99/month × 12 × 5 = $239
- Self-hosted Bitwarden: $40/year (VPS) + 1 hour/month maintenance (~$120 in billable labor if you value your time) = $640 in setup + ~$200 in labor = $840 total