A complete field guide to self-hosting Vaultwarden — Docker setup, HTTPS that configures itself, backups done right, and the security gotchas that catch almost everyone.

Your password manager is the single most sensitive database you own. Every login, every 2FA recovery code, every credit card number sits in one vault. Most people park that vault on someone else's servers and pay a subscription for the privilege. Bitwarden's hosted service is genuinely good — but if you already run a VPS, there's a compelling middle path: Vaultwarden, a lightweight, community-written reimplementation of the Bitwarden server API that sips resources instead of gulping them.

The official Bitwarden server is a fleet of .NET containers that wants multiple gigabytes of RAM. Vaultwarden is a single Rust binary in one container that idles at around 30 MB. It runs happily on the cheapest VPS tier, and in my experience it simply never pages you. Here's the whole setup, including the parts the quickstart guides gloss over.

What you need

  • A VPS with at least 1 GB RAM (512 MB works, but don't).
  • A domain or subdomain you control, e.g. vault.yourdomain.com.
  • Docker and Docker Compose installed.
  • About 30 minutes.

1. Point DNS at your VPS

Create an A record for your subdomain pointing at your VPS IP. No CDN proxy games — you want the reverse proxy on the VPS to answer the ACME challenge directly. Wait for propagation (dig +short vault.yourdomain.com) before moving on, or your first certificate issuance will fail and you'll be rate-limited into a coffee break.

2. Let Caddy handle HTTPS

You can use nginx with certbot. I used to. Then I switched every small service to Caddy and never looked back, because the entire TLS story — issuance, renewal, redirects — is three lines of config:

vault.yourdomain.com {
    reverse_proxy 127.0.0.1:8080
}

That's it. Caddy fetches the Let's Encrypt certificate, renews it, and redirects HTTP to HTTPS. Run it as a container or a system service; either way, it listens on ports 80 and 443.

3. The Compose file

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.yourdomain.com"
      SIGNUPS_ALLOWED: "false"
      INVITATIONS_ALLOWED: "true"
      ADMIN_TOKEN: "<argon2-hash>"
    volumes:
      - ./vw-data:/data
    ports:
      - "127.0.0.1:8080:80"

A few deliberate choices here. Binding to 127.0.0.1:8080 means Vaultwarden is never directly reachable from the internet — all traffic goes through Caddy, which terminates TLS. restart: unless-stopped is the difference between a VPS reboot being a non-event and a 6 a.m. incident.

Bring it up: docker compose up -d. The web vault is now live at your domain.

4. The environment variables that actually matter

DOMAIN must be exact. Protocol, hostname, no trailing slash. Vaultwarden uses it to build invitation links, 2FA callback URLs, and CORS headers. Get it wrong and you'll chase phantom "login failed" errors that are really CORS rejections. I know this because I did it.

SIGNUPS_ALLOWED starts as true for exactly one session. Leave public registration open on first boot, create your account, then flip it to false and redeploy. Bots scan for open Vaultwarden instances within hours of them appearing — this is not theoretical. Keep INVITATIONS_ALLOWED: "true" so you can still add family members through the organization invite flow, which respects your domain's allowlist.

ADMIN_TOKEN should be an Argon2 hash, not plaintext. Generate one with the image itself:

docker run --rm -it vaultwarden/server /vaultwarden hash

Paste the resulting $argon2id$... string into the compose file. The admin panel lives at https://vault.yourdomain.com/admin and lets you manage users, invitations, and diagnostics. Guard it like it guards you.

WebSocket notifications need nothing extra. Since Vaultwarden 1.30, live sync rides the same HTTP port via upgrade requests — no separate port, no extra proxy block. Caddy handles the upgrade transparently. If your vault syncs on your phone but changes don't appear until you pull-to-refresh, your proxy is eating the upgrade headers; with the config above, it isn't.

5. Point your apps at it

Bitwarden's official clients — desktop, mobile, browser extension, CLI — all let you set a custom server URL. Log out of the hosted service (or better, export an encrypted JSON backup first), point the client at https://vault.yourdomain.com, and create your account there. Push notifications on mobile work out of the box; Vaultwarden relays them through Bitwarden's push service by default.

Set up 2FA on the Vaultwarden account itself — TOTP via an authenticator app — and store the recovery code somewhere physical. Losing both your vault password and your 2FA device with no recovery path is the one unrecoverable failure mode. Plan for it now.

6. Backups: the part everyone skips

Vaultwarden stores everything in a SQLite database at /data/db.sqlite3 (plus attachments/ and sends/ directories beside it). Do not back up the live database file with a plain copy — SQLite's write-ahead log means a file-level copy taken mid-write can be corrupt.

The safe way is SQLite's own online backup, which works while the container is running:

sqlite3 ./vw-data/db.sqlite3 ".backup './backups/db-$(date +%F).sqlite3'"

That needs the sqlite3 CLI on the host. Then bundle it with the attachments directory, encrypt the bundle, and ship it off the VPS. A minimal nightly cron job:

#!/bin/bash
set -euo pipefail
DATE=$(date +%F)
BACKUP_DIR=/opt/vaultwarden/backups
mkdir -p "$BACKUP_DIR"
sqlite3 /opt/vaultwarden/vw-data/db.sqlite3 ".backup '$BACKUP_DIR/db-$DATE.sqlite3'"
tar -czf "$BACKUP_DIR/vw-files-$DATE.tar.gz" -C /opt/vaultwarden/vw-data attachments sends
# encrypt + upload to your offsite storage here (restic, rclone, rsync…)
find "$BACKUP_DIR" -type f -mtime +14 -delete

Test your restore at least once. A backup you have never restored is a backup you do not have — spin up a second compose stack on a different port, drop in the backup files, and log in.

7. Upgrades without drama

Upgrading is docker compose pull && docker compose up -d, but add one step first: take a backup. Vaultwarden's database migrations run automatically on startup and are one-way. If a release ever misbehaves, your escape hatch is the backup from five minutes ago, not a GitHub issue.

Watch the release notes for breaking changes (the SIGNUPS_ALLOWED default and admin-token hashing changes both arrived this way), and pin to a specific version tag in production if you'd rather upgrade on your own schedule: image: vaultwarden/server:1.34.2.

The gotchas, rapid-fire

  • Open registration left on. The single most common Vaultwarden misconfiguration. Flip it off the moment your account exists.
  • Forgetting DOMAIN. Wrong-URL invitation links and mysterious CORS failures, all from one env var.
  • Exposing port 80 directly. Always bind to localhost and proxy through TLS. Vaultwarden speaks plain HTTP by design; the proxy is your encryption.
  • Unencrypted backups. Your backup contains every password you own. Encrypt it, or your backup strategy is a breach waiting to happen.
  • No SMTP configured. Email 2FA and organization invites need an SMTP server. Set SMTP_HOST and friends, or you'll discover this when a family member can't accept an invite.
  • Skipping the restore test. Covered above. Do it once.

The honest tradeoff

Self-hosting your password manager means you are your own SRE: uptime, backups, and patching are on you. But if you already maintain a VPS, Vaultwarden is the rare self-hosted service that is genuinely less moving parts than the SaaS alternative — one container, one database file, one cron job.

Sources