Auto Draft

Self-Hosting Vaultwarden: Run Your Own Bitwarden Password Manager on Docker

Why Self-Host Your Password Manager?

If you’re serious about security, you’re already using a password manager. But handing your entire digital life to a third-party SaaS — even one with a good reputation — means trusting their infrastructure, their uptime, and their response to breaches. LastPass had a major breach in 2022 that exposed encrypted vaults. Bitwarden’s cloud is well-run, but you’re still dependent on their servers.

Vaultwarden (formerly known as bitwarden_rs) is an unofficial, community-developed Bitwarden server implementation written in Rust. It’s lightweight, fully compatible with all official Bitwarden clients (iOS, Android, Chrome, Firefox, desktop), and runs comfortably on a Raspberry Pi, a spare VM, or a single Docker container in your homelab. You get the same end-to-end encryption and polished client apps — just with your own server behind them.

In this guide, we’ll set up a production-ready Vaultwarden instance using Docker Compose with HTTPS (via Nginx reverse proxy and Let’s Encrypt), automatic database backups, Fail2ban protection, and secure admin access. If you’ve worked through the complete Docker beginner’s guide or already have containers running in your homelab, you’re ready to follow along.

What You’ll Need

  • A Linux server or VM (Debian 12, Ubuntu 22.04+, or Raspberry Pi OS 64-bit)
  • Docker and Docker Compose installed (docker compose v2 plugin)
  • A domain name you control with a subdomain pointed at your server — e.g., vault.yourdomain.com
  • Ports 80 and 443 reachable from the internet (or use Cloudflare Tunnel if you’re behind CGNAT)
  • At least 512 MB RAM — Vaultwarden is genuinely lean, usually sitting around 30–60 MB at rest
  • Basic familiarity with Docker Compose and Nginx config syntax

If you want to keep this strictly internal with no public exposure, skip the HTTPS section and pair Vaultwarden with Tailscale instead. Bitwarden clients work perfectly over a private VPN mesh — just point them at your Tailscale IP.

Project Directory Layout

Keep everything organized from the start:

mkdir -p ~/vaultwarden/{data,nginx,backups}
cd ~/vaultwarden

Your final structure will look like this:

~/vaultwarden/
├── docker-compose.yml
├── .env
├── nginx/
│   └── vaultwarden.conf
├── data/             ← Vaultwarden's SQLite DB and attachments
└── backups/          ← nightly DB snapshots

Step 1: Write the docker-compose.yml

Create docker-compose.yml in your project directory. This defines three services: Vaultwarden itself, an Nginx reverse proxy for TLS termination, and a lightweight backup sidecar.

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - ./data:/data
    environment:
      DOMAIN: "https://${VW_DOMAIN}"
      SIGNUPS_ALLOWED: "false"
      INVITATIONS_ALLOWED: "true"
      ADMIN_TOKEN: "${VW_ADMIN_TOKEN}"
      SMTP_HOST: "${SMTP_HOST}"
      SMTP_FROM: "${SMTP_FROM}"
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "${SMTP_USER}"
      SMTP_PASSWORD: "${SMTP_PASS}"
      LOG_LEVEL: "warn"
      WEBSOCKET_ENABLED: "true"
      WEBSOCKET_PORT: "3012"
    networks:
      - vw_net

  nginx:
    image: nginx:stable-alpine
    container_name: vw_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/vaultwarden.conf:/etc/nginx/conf.d/default.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /var/www/certbot:/var/www/certbot:ro
    depends_on:
      - vaultwarden
    networks:
      - vw_net

  backup:
    image: alpine:latest
    container_name: vw_backup
    restart: unless-stopped
    volumes:
      - ./data:/data:ro
      - ./backups:/backups
    entrypoint: >
      sh -c "while true; do
        TIMESTAMP=$$(date +%Y%m%d_%H%M%S);
        cp /data/db.sqlite3 /backups/vaultwarden_$$TIMESTAMP.sqlite3;
        find /backups -name '*.sqlite3' -mtime +7 -delete;
        echo \"Backed up at $$TIMESTAMP\";
        sleep 86400;
      done"

networks:
  vw_net:
    driver: bridge

Key settings worth understanding:

  • SIGNUPS_ALLOWED: false — We’ll create our first account with signups on, then disable it. Anyone wanting access afterward must be invited through the admin panel.
  • WEBSOCKET_ENABLED: true — Required for real-time vault sync across Bitwarden clients. Without it, changes on one device may take minutes to appear on another.
  • ADMIN_TOKEN — Protects the /admin panel. Generate a strong token; we’ll cover that next.

Step 2: Configure Environment Variables

Create a .env file alongside your docker-compose.yml. Keep this out of version control — add it to .gitignore if you’re tracking this directory in a repo.

VW_DOMAIN=vault.yourdomain.com
VW_ADMIN_TOKEN=your_very_long_random_token_here
SMTP_HOST=smtp.example.com
SMTP_FROM=vault@yourdomain.com
SMTP_USER=your_smtp_username
SMTP_PASS=your_smtp_password

Generate a cryptographically secure admin token with:

openssl rand -base64 48

Paste that output as VW_ADMIN_TOKEN. If you want an extra layer of protection, Vaultwarden 1.29+ also supports argon2 hashing for the admin token — run vaultwarden hash --preset owasp inside the container and use the resulting hash instead of plaintext.

Step 3: Nginx Reverse Proxy Configuration

Create nginx/vaultwarden.conf. This config handles the HTTP-to-HTTPS redirect, TLS termination, security headers, and the WebSocket proxy for live sync:

server {
    listen 80;
    server_name vault.yourdomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name vault.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/vault.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy no-referrer always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' wss:;" always;

    client_max_body_size 128M;

    # WebSocket for real-time sync
    location /notifications/hub {
        proxy_pass http://vaultwarden:3012;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    location / {
        proxy_pass http://vaultwarden:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Replace both instances of vault.yourdomain.com with your actual subdomain before proceeding.

Step 4: Obtain TLS Certificates with Certbot

Make sure your DNS A record is already pointing at your server before running Certbot. Install it on the host (not inside Docker):

sudo apt install certbot -y

sudo certbot certonly --webroot \
  -w /var/www/certbot \
  -d vault.yourdomain.com \
  --email you@yourdomain.com \
  --agree-tos \
  --non-interactive

Your certificate will land in /etc/letsencrypt/live/vault.yourdomain.com/. Enable auto-renewal:

sudo systemctl enable --now certbot.timer

To reload Nginx after a renewal, add a deploy hook:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
docker exec vw_nginx nginx -s reload
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Step 5: First Launch and Account Setup

Before first launch, temporarily set SIGNUPS_ALLOWED: "true" in your docker-compose.yml. Then bring everything up:

docker compose up -d
docker compose logs -f vaultwarden

You should see Vaultwarden log:

[INFO] Rocket has launched from http://0.0.0.0:80
[INFO] Starting WebSocket server on 0.0.0.0:3012

Navigate to https://vault.yourdomain.com, create your account, and verify your email. Then immediately flip SIGNUPS_ALLOWED back to "false" and redeploy:

docker compose up -d

From this point, new users can only join via invite from the admin panel.

Step 6: Accessing the Admin Panel and Inviting Users

Navigate to https://vault.yourdomain.com/admin and enter your VW_ADMIN_TOKEN. From here you can:

  • Invite users by email — even with signups disabled, you can bring in family members or teammates one at a time
  • Manage organizations — create a shared vault for family passwords (Netflix, home WiFi, shared accounts)
  • Run diagnostics — check server version, database stats, SMTP connectivity, and WebSocket status
  • Force 2FA — require two-factor authentication for all accounts on your instance
  • View active sessions — see connected devices and revoke them if needed

Vaultwarden supports TOTP (Google Authenticator, Aegis), FIDO2/WebAuthn hardware keys (YubiKey, etc.), and email-based 2FA. Enable it on every account — the admin panel lets you enforce this globally.

Step 7: Database Backups

Vaultwarden stores everything in a single SQLite file at ./data/db.sqlite3. The backup sidecar container defined earlier handles daily snapshots and a 7-day rolling retention. You can also trigger an on-demand consistent backup without stopping the server:

docker exec vaultwarden sqlite3 /data/db.sqlite3 \
  ".backup /data/db_backup_manual.sqlite3"

For off-site redundancy, push backups to an S3-compatible bucket with rclone:

rclone copy ~/vaultwarden/backups/ \
  remote:vaultwarden-backups/ \
  --min-age 1h

Store at least one copy somewhere outside your homelab. A backup that only exists on the same machine as the primary data isn’t really a backup. This is the same discipline you’d apply to your Nextcloud instance or any other critical self-hosted service.

Step 8: Hardening with Fail2ban

Vaultwarden logs failed login attempts — Fail2ban can watch those logs and ban repeat offenders automatically. Install Fail2ban on the host:

sudo apt install fail2ban -y

Create a Vaultwarden filter:

# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <HOST>.*$
ignoreregex =

Create a jail config:

# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled  = true
port     = 80,443
filter   = vaultwarden
logpath  = /home/youruser/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime  = 3600
findtime = 600
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden

Five failed attempts within 10 minutes results in a 1-hour ban. Adjust bantime and maxretry to taste.

Step 9: Installing Bitwarden Clients

All official Bitwarden clients work with Vaultwarden without modification. When signing in, tap “Self-hosted” or look for the server URL field:

  • Browser extensions: Available for Chrome, Firefox, Safari, and Edge. Install from official extension stores, then on the login screen click the gear icon and set the Server URL.
  • Desktop apps: Windows, macOS, and Linux packages at the Bitwarden website. Same gear icon flow.
  • iOS / Android: App Store or Google Play. Tap the gear icon before logging in.
  • CLI: bw config server https://vault.yourdomain.com then bw login

Bitwarden uses end-to-end AES-256 encryption. Your Vaultwarden server only ever stores ciphertext — not plaintext passwords. Even if someone gained access to your db.sqlite3 file, they’d need your master password to decrypt any entries.

Using Bitwarden Send and Secure Notes

Beyond storing passwords, Vaultwarden supports two features worth knowing about: Bitwarden Send and Secure Notes.

Bitwarden Send lets you create a temporary encrypted link to share a password, text, or file with someone who doesn’t have a Bitwarden account. You set an expiration time, an optional access password, and a deletion date. The recipient gets a link; they see the plaintext for the duration you specified. It’s ideal for securely sharing a Wi-Fi password with a houseguest or sending a one-time access credential to a contractor.

From the CLI:

bw send create --name "Guest WiFi" --text "yourpassword123" \
  --deletion-date "2026-06-01" --expiration-date "2026-05-15"

Secure Notes are encrypted free-form text entries — useful for storing SSH key passphrases, software license keys, bank account details, or recovery codes for your TOTP app. They live alongside your passwords in the same encrypted vault and sync across all clients.

For SSH keys themselves, you can store the private key as a file attachment on a secure note entry. This keeps your key material encrypted at rest and synced across machines — though for high-security scenarios, a hardware key remains preferable.

Keeping Vaultwarden Updated

Vaultwarden releases are frequent and worth keeping up with — security patches, new Bitwarden client compatibility, and performance improvements all land regularly. Updating is a two-command operation:

docker compose pull vaultwarden
docker compose up -d vaultwarden

There’s no database migration step to worry about. Vaultwarden handles schema updates automatically on startup. Before any major update, take a manual backup just in case:

docker exec vaultwarden sqlite3 /data/db.sqlite3 \
  ".backup /data/db_pre_update.sqlite3"

Troubleshooting Common Issues

Clients show “invalid certificate” errors

Run sudo certbot renew --dry-run to verify renewal works. Check that Nginx is loading the correct certificate path and that the cert hasn’t expired. Bitwarden clients are strict about certificate validity and won’t connect to self-signed certs unless you’ve manually trusted the CA.

WebSocket sync isn’t working

Check that the /notifications/hub location block is in your Nginx config and that WEBSOCKET_ENABLED=true is set in your environment. Verify the WebSocket upgrade is passing through:

curl -s -o /dev/null -w "%{http_code}" \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  https://vault.yourdomain.com/notifications/hub

Email invites not sending

Test SMTP from the admin panel at /admin/diagnostics. Check container logs:

docker compose logs vaultwarden | grep -i smtp

Common issue: some hosting providers block outbound port 25 and 587. Try switching to port 465 with SMTP_SECURITY: "force_tls".

Database locked after unclean shutdown

docker exec vaultwarden sqlite3 /data/db.sqlite3 "PRAGMA integrity_check;"

If integrity check returns anything other than ok, restore from your most recent backup.

Migrating From LastPass, 1Password, or Bitwarden Cloud

Bitwarden Cloud users have the smoothest migration: export a .json from the Bitwarden web vault (Account → Export Vault), then import it at https://vault.yourdomain.com/#/tools/import. All passwords, secure notes, identities, and cards transfer cleanly, including folder structure.

For LastPass and 1Password, export as CSV from their respective apps, then use Bitwarden’s importer — select the correct source format from the dropdown. Bitwarden supports over 50 password manager import formats. Check your import for completeness after finishing, especially for items with file attachments (those must be re-uploaded manually).

Final Thoughts

Vaultwarden is one of the best self-hosting projects available for homelab builders. It’s mature, actively maintained by the open-source community, genuinely lightweight, and backed by polished official Bitwarden clients on every major platform. Running your own means no subscription cost, no third-party data custody, and complete visibility into what’s stored and where.

The full setup — from fresh VM to working password manager with TLS, backups, and Fail2ban — takes about two hours. After that, the only maintenance is pulling updates every few months and checking your backup folder occasionally. If you’re already running other Docker workloads in your homelab, this drops right into your existing workflow. If you’re new to Docker-based self-hosting, the Docker beginner’s guide linked at the top of this post is the right place to start. And if you want to access your vault securely from anywhere without opening firewall ports, pairing it with Tailscale is an elegant solution that keeps the whole stack private.

Self-hosting your password manager isn’t paranoia — it’s a straightforward operational security decision that pays dividends for years. Give it an afternoon and you’ll wonder why you didn’t do it sooner.

Enjoying this post?

Get more guides like this delivered straight to your inbox. No spam, just tech and trails.