Hacking, Code & Open Source Reads

Vaultwarden - Self-Hosting the Bitwarden Server

Christian Lehnert2021-06-19~7 min read

Bitwarden is the password manager that won the post-LastPass migration. Open source under GPL, audited regularly, with native clients on every platform that matters and a server you can self-host. The official Bitwarden server is a Docker compose stack with around eleven containers — MSSQL, identity service, API service, admin portal, attachments service, notifications service, and several others. It works, it is well-tested, and it is overkill for personal or small-team self-hosting.

Daniel García's bitwarden_rs — renamed to Vaultwarden in May at Bitwarden's request to avoid trademark confusion — solves the same problem with one container, one binary, written in Rust, with a SQLite or PostgreSQL backend. It implements the Bitwarden API faithfully enough that every official Bitwarden client works against it without modification: iOS app, Android app, Firefox extension, Chrome extension, desktop app, CLI. Your data lives on your server. The clients you use are the official ones. The trust boundary is your VPS, not Bitwarden's hosted infrastructure.

This post is about why Vaultwarden is the right answer for self-hosted password management in 2021, how to deploy it cleanly, and the honest caveats about running it.

Why Vaultwarden over the official server

Three reasons, in increasing order of weight:

Resource footprint. The official Bitwarden server stack uses roughly 1.5–2 GB of RAM at idle and substantial CPU for the .NET-based services. Vaultwarden uses about 50–100 MB of RAM and minimal CPU. For a single user or a small family, the difference is the difference between needing a $20/month VPS and being fine with a $5/month one. Over a year, this matters.

Operational simplicity. One container, one binary, one config file, one database. No multi-service orchestration. No service-to-service certificate management. Backup is "copy the data directory." Upgrade is "pull new image, restart container." There is genuinely less to manage.

Source-available implementation. Vaultwarden is GPLv3, written in Rust, with about 30,000 lines of source code. A motivated reader can audit it in a weekend. The official Bitwarden server is also open source, but the .NET codebase plus the SQL Server dependency plus the multi-service architecture make it substantially harder to reason about.

The official server is the right choice for organizations of more than a few dozen users where the additional features — SSO, SCIM provisioning, advanced audit logging — matter. For everyone else, Vaultwarden is the better fit.

Deployment on Debian 10 Buster

The recommended deployment is via Docker. Install Docker first if you have not:

1sudo apt-get install docker.io docker-compose
2sudo systemctl enable --now docker

Create a directory for the data and a Compose file:

1sudo mkdir -p /srv/vaultwarden/data
2cd /srv/vaultwarden

compose.yml:

 1version: "3"
 2 
 3services:
 4  vaultwarden:
 5    image: vaultwarden/server:latest
 6    container_name: vaultwarden
 7    restart: unless-stopped
 8    environment:
 9      DOMAIN: "https://vault.example.com"
10      WEBSOCKET_ENABLED: "true"
11      SIGNUPS_ALLOWED: "false"
12      INVITATIONS_ALLOWED: "true"
13      ADMIN_TOKEN: "GENERATED_RANDOM_TOKEN_HERE"
14      SMTP_HOST: "mail.example.com"
15      SMTP_FROM: "vault@example.com"
16      SMTP_PORT: "587"
17      SMTP_SECURITY: "starttls"
18      SMTP_USERNAME: "vault@example.com"
19      SMTP_PASSWORD: "SMTP_PASSWORD_HERE"
20    volumes:
21      - ./data:/data
22    ports:
23      - "127.0.0.1:8080:80"
24      - "127.0.0.1:3012:3012"

Generate the admin token with reasonable entropy:

1openssl rand -base64 48

Replace GENERATED_RANDOM_TOKEN_HERE in the Compose file. Bring it up:

1docker-compose up -d

The bind to 127.0.0.1:8080 and 127.0.0.1:3012 is deliberate — Vaultwarden is bound to localhost, and a reverse proxy fronts it with TLS. The 3012 port is the WebSocket endpoint that Bitwarden clients use for real-time vault sync; without it, vault changes only sync on full client refreshes.

Reverse proxy with Caddy

Caddy v2 is the cleanest fit. The Caddyfile entry:

vault.example.com {
    reverse_proxy /notifications/hub localhost:3012
    reverse_proxy localhost:8080
    
    encode gzip
    
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "no-referrer"
    }
}

Reload:

1sudo systemctl reload caddy

Caddy obtains the TLS certificate automatically. The two reverse_proxy lines route the WebSocket endpoint (which clients hit at /notifications/hub) separately from the main HTTP traffic. Without the WebSocket route, the clients still work but lose real-time sync.

For nginx-based deployments, the equivalent server block is straightforward but longer; the official Vaultwarden wiki documents it.

First-time setup

Visit https://vault.example.com in a browser. The first-time experience is identical to the official Bitwarden web vault — you create an account with email, master password, and an optional hint. Master-password requirements are enforced by the client, not the server; pick a passphrase you can memorize.

Two security configurations to do immediately:

Enable 2FA. Settings → Two-step Login → Authenticator App. Use any TOTP authenticator (the Bitwarden app itself can do this, or any standard authenticator). Print or store the recovery code.

Disable signups. The SIGNUPS_ALLOWED: "false" in the Compose file means new users cannot register through the public web vault. Existing users (you, family members) are added by invitation through the admin portal. The admin portal lives at https://vault.example.com/admin and requires the ADMIN_TOKEN you generated.

For an SMTP-equipped Vaultwarden, invitations work as expected — you enter an email, the recipient receives an invitation link. Without SMTP, you can still invite users but they need the invitation link delivered out of band.

Backup strategy

The data directory at /srv/vaultwarden/data/ contains:

  • db.sqlite3 — the encrypted vault data
  • attachments/ — file attachments uploaded by users
  • sends/ — Bitwarden Send temporary files
  • config.json — server configuration
  • rsa_key.pem, rsa_key.pub.pem — server keys
    A complete backup is a copy of this directory. The vault data is already encrypted at the field level by client-side keys derived from each user's master password — Vaultwarden never sees plaintext vault contents. So a backup of the data directory leaks structural metadata (folder names, item counts, last-modified times) but not the actual passwords.

For a production backup, a Borg or Restic snapshot of the data directory, taken nightly, on a separate host, with encryption, is sufficient. The previous post on Borg covers the mechanics; the same patterns apply.

If you are using SQLite (the default), sqlite3 db.sqlite3 .backup db-backup.sqlite3 produces a consistent snapshot without stopping Vaultwarden. For PostgreSQL, use pg_dump against the database container.

Caveats worth knowing

Three things that the Vaultwarden README does not emphasize enough.

Not officially supported. Bitwarden the company does not endorse, support, or test against Vaultwarden. If a client breaks against Vaultwarden after a Bitwarden client update, the fix is in Vaultwarden, not Bitwarden — and depends on Daniel García or contributors implementing the change. In practice, Vaultwarden has tracked Bitwarden's API changes closely for years, and major breaking changes are rare. But the dependency is real.

The admin portal is powerful. Anyone with the ADMIN_TOKEN can create users, modify users, change SMTP settings, and read configuration. The admin portal must be reachable only over TLS, ideally restricted by IP, ideally behind a VPN, and the token rotated if you suspect compromise. Treat the token like an SSH private key.

No SCIM, no SAML, no enterprise SSO. If you need to integrate with an existing identity provider, Vaultwarden does not support it. The official Bitwarden server does, at the cost of the architectural complexity discussed above.

Database backups must be tested. A backup that has never been restored is not a backup. After your first month of operation, do a dry-run restore on a test container and confirm the data is intact. The pain of discovering during an incident that the backups were silently corrupted for six months is a pain you can avoid with a thirty-minute test.

When this is right

Vaultwarden is right for:

  • Individual users who want their password vault on infrastructure they control.

  • Small families or groups (under twenty users) sharing a self-hosted vault.

  • Privacy-conscious operators who do not want their password metadata flowing through a SaaS provider's network.

  • Anyone running a homelab who wants to consolidate identity-adjacent services.
    Vaultwarden is not right for:

  • Organizations needing SSO integration, SCIM provisioning, or compliance certifications.

  • Teams larger than a few dozen users where the official Bitwarden Enterprise features matter.

  • Anyone who is not willing to take operational responsibility for the server and its backups.

The summary

Vaultwarden is a Rust reimplementation of the Bitwarden server, smaller and simpler than the official version, fully API-compatible with the official clients, and well-suited to self-hosted password management for individuals and small teams. The deployment is fifteen minutes. The maintenance is occasional security updates. The trust boundary is your VPS instead of someone else's data center.

In a world where centralized identity infrastructure has become a single point of catastrophic failure — LastPass, Okta, every SSO provider that has had a major incident in the last five years — running your own password vault on a small Rust server you understand is not paranoia. It is reasonable hygiene.

Tagged:
#selfhosted #security #linux #rust
← Back to posts