Self-Hosted Password Manager with Bitwarden on Docker

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:

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
Watch out: Replace 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:

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
Tip: Test your restore procedure once. A backup that hasn't been tested is just noise. Spin up a second docker-compose instance on localhost, extract your tarball, and verify you can log in with your master password. If disaster strikes at 3 AM, you'll be glad you practiced.

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:

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: