self-hosted Vaultwarden self-hosted Vaultwarden

Deploying a Self-Hosted Password Manager (Vaultwarden) in Under an Hour

A password manager you don’t control is a single point of failure you’ve outsourced to someone else’s threat model. Vaultwarden — the lightweight Rust reimplementation of the Bitwarden server — runs on roughly 50 MB of RAM, idles happily on a Raspberry Pi or a $3 VPS, and speaks the full Bitwarden API so every official browser extension, mobile app, and desktop client connects to it without modification. The current stable release is 1.35.4, published on February 23, 2026, and a clean deployment from blank Linux box to working vault takes about 45 minutes if you do it once and follow the steps in order.

This guide walks the path most homelab and small-team operators actually need: Docker Compose, a real domain with HTTPS, a Caddy reverse proxy, an Argon2id-hashed admin token, signups locked down after first login, and a backup script you’ll actually run. It assumes a Debian or Ubuntu host with ssh access and a domain you can point at it.

Why Vaultwarden Instead of Bitwarden Self-Host

Bitwarden’s official self-hosted server works, but it ships as a stack of containers fronted by MSSQL and expects 2 GB of RAM as a floor. Vaultwarden replaces that with a single Rust binary backed by SQLite, runs comfortably under 100 MB of RAM, and unlocks features Bitwarden gates behind a Premium subscription — TOTP storage, file attachments, Bitwarden Send, and emergency access — at no cost.

The project is maintained by Daniel García with active contributors including BlackDex, Timshel, and dfunkt. As of release 1.35.0, one of the active maintainers is employed by Bitwarden and contributes on personal time, an unusual but documented relationship that has so far produced cleaner upstream API compatibility rather than friction. The 1.35 line added OpenID Connect SSO (PR #3899), bumped the bundled web vault to 2025.12.0, added support for mobile clients on the 2026.1.0+ series, and shipped as the first immutable release with attestation — meaningful for anyone who cares about supply-chain integrity on a credential store.

The trade-off is operational. You own the uptime, the patching, and the backups. A dead Vaultwarden instance is a vault you can’t open and a TOTP set you can’t reach. Plan accordingly.

Prerequisites and Host Sizing

A 1 vCPU / 1 GB RAM VPS is overkill; a 512 MB instance handles a family vault with room to spare. The constraints that actually matter:

A real domain — vault.example.com — with DNS pointed at the host’s public IP. The Bitwarden web vault uses the Web Crypto API, which only runs in a secure context. That means https:// everywhere or http://localhost; there is no flag to bypass it. Browser extensions and mobile apps will refuse to connect to a plain-HTTP instance.

Docker Engine 24.0 or newer with the Compose v2 plugin. On Ubuntu 24.04 or Debian 13:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

Log out and back in so the group change takes effect. Open ports 80 and 443 in your firewall — ufw allow 80,443/tcp on Ubuntu — and nothing else from the public internet. Vaultwarden itself binds only to localhost; the proxy handles the world-facing TLS.

The Compose File

Create /opt/vaultwarden/ and a compose.yaml inside it. The container image is published to ghcr.io, docker.io, and quay.io; the official tag is vaultwarden/server:latest from Docker Hub, but pinning a specific version is wiser for a credential store — bumping happens on your schedule, not theirs.

services:
  vaultwarden:
    image: vaultwarden/server:1.35.4
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
      ADMIN_TOKEN: "$argon2id$v=19$m=65540,t=3,p=4$..."
      SMTP_HOST: "smtp.example.com"
      SMTP_FROM: "[email protected]"
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "[email protected]"
      SMTP_PASSWORD: "app-specific-password"
    volumes:
      - ./vw-data/:/data/
    ports:
      - 127.0.0.1:8000:80

SIGNUPS_ALLOWED is true only long enough to register your own account, then flipped off. Binding to 127.0.0.1:8000 means the container is unreachable from the public internet until the reverse proxy is in place — a deliberate safety interlock, not an oversight.

Generating an Argon2id Admin Token

The /admin panel is where you manage users, organizations, and runtime configuration without restarting the container. Recent Vaultwarden versions reject plaintext admin tokens and expect an Argon2id hash — a secret derived with a memory-hard KDF that survives database leaks far better than a bare string.

Generate the hash on the host:

echo -n "your-strong-admin-password" \
  | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4

Copy the entire output, starting with $argon2id$v=19$..., into the ADMIN_TOKEN field of your compose file. When you visit /admin in the browser, you’ll type the original password, not the hash — Vaultwarden verifies it against the stored derivation. Treat the original password like a root credential, because functionally it is one.

Reverse Proxy with Caddy

Caddy is the path of least resistance: it handles Let’s Encrypt automatically, the config is a few lines, and it doesn’t require a separate certbot cron job. Install on Debian/Ubuntu with the official APT repo, then write /etc/caddy/Caddyfile:

vault.example.com {
    reverse_proxy 127.0.0.1:8000
    encode zstd gzip
    header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}

systemctl reload caddy, wait a few seconds for the ACME challenge, and https://vault.example.com is live with a valid certificate. Nginx and Traefik configurations are in the Vaultwarden wiki under proxy examples; use whichever you already operate, but Caddy’s defaults are correct out of the box and that matters when the thing you’re protecting is every password you own.

Bring the stack up with docker compose up -d, watch docker compose logs -f for clean startup, then load the URL. Register your account, log in once, then edit the compose file to set SIGNUPS_ALLOWED: "false" and run docker compose up -d again to apply.

Configuration Reference

The environment variables you’ll actually touch on a fresh deployment, and what they do.

REFERENCE
Core Vaultwarden environment variables
DOMAIN
Full HTTPS URL of your instance. Required. Used in invitation emails, push notifications, and WebAuthn origin checks. Wrong value here breaks 2FA enrollment silently.
ADMIN_TOKEN
Argon2id hash of your admin-panel password. Leave unset to disable /admin entirely. Generate with the argon2 CLI; never use a plaintext value.
SIGNUPS_ALLOWED
Open registration. Set true only during initial setup, then flip to false. New users invited from the admin panel after that.
SIGNUPS_DOMAINS_WHITELIST
Comma-separated email domains permitted to self-register when signups are open. Useful for invite-by-email workflows.
SMTP_HOST / SMTP_PORT / SMTP_SECURITY
Outbound mail relay. Required for email 2FA, password-reset hints, and admin invitations. Without SMTP, invitations require manual link copying.
PUSH_ENABLED / PUSH_INSTALLATION_ID
Enables Bitwarden’s push relay for real-time mobile sync. Requires registering an installation ID at bitwarden.com/host. Without it, mobile clients sync on a polling interval.
SSO_ENABLED / SSO_AUTHORITY / SSO_CLIENT_ID
OpenID Connect SSO, added in 1.35.0. Lets users authenticate via Authentik, Keycloak, or any OIDC provider before unlocking their vault with the master password.
DATABASE_URL
SQLite by default at /data/db.sqlite3. Set to a MySQL/MariaDB or PostgreSQL connection string for larger deployments. SQLite is the right answer for under ~50 users.

The full reference is in the Vaultwarden wiki — over 100 variables exist, but the eight above cover what a fresh deployment actually needs.

Hardening the Instance

Three settings move a working deployment to a defensible one.

Enable two-factor authentication on your own account immediately. Settings → Security → Two-step Login. Vaultwarden supports TOTP (Aegis, 2FAS, Google Authenticator), email codes, and FIDO2/WebAuthn hardware keys. A Yubikey or equivalent on at least one account is the right baseline; if your master password leaks, the hardware factor stands between an attacker and the vault.

Add Fail2Ban or your reverse proxy’s equivalent rate-limiting against /admin and /identity/connect/token. The Vaultwarden wiki publishes a working jail config that parses the container logs and bans IPs after repeated failures. Without it, a dictionary attack against the admin panel runs unimpeded.

Disable the admin panel entirely once you no longer need it. Comment out ADMIN_TOKEN and restart the container; /admin returns 404. Re-enable only when you need to invite users or change settings, then disable again. The fewer authenticated surfaces facing the internet, the better.

Backups Are the Whole Point

A self-hosted password manager without backups is a worse outcome than a cloud password manager. SQLite supports hot backups via its own .backup command, which is the only safe way to copy the database while the container is running.

#!/bin/bash
DEST=/backup/vaultwarden/$(date +%Y-%m-%d)
mkdir -p "$DEST"
sqlite3 /opt/vaultwarden/vw-data/db.sqlite3 ".backup '$DEST/db.sqlite3'"
cp -r /opt/vaultwarden/vw-data/attachments "$DEST/" 2>/dev/null
cp /opt/vaultwarden/vw-data/rsa_key* "$DEST/"
find /backup/vaultwarden -type d -mtime +30 -exec rm -rf {} +

Run it daily from cron (0 3 * * *), encrypt the output with age or gpg, and replicate offsite — restic to S3-compatible storage works well. The rsa_key* files are the server’s signing keys; without them, restored data still decrypts (encryption keys live with the user) but device-trust state and push registrations break.

Test the restore. A backup you have not verified is a hope, not a backup. Spin up a second instance from a backup at least once before you need to.

Common Pitfalls

The Web Crypto API requirement catches first-time deployers constantly. If the web vault loads but login spins forever, the symptom is almost always a non-HTTPS context — either a missing certificate, a self-signed cert the browser doesn’t trust, or a DOMAIN value that doesn’t match the URL you’re actually loading. Check the browser console; failures here surface as cryptic crypto errors, not auth errors.

Mobile apps require a working DOMAIN and, ideally, push relay configured. Without push, a vault edit on desktop won’t propagate to the phone until the next manual sync. The setup is documented at bitwarden.com/host and takes about five minutes.

Master password loss is unrecoverable. Vaultwarden uses the same client-side encryption model as Bitwarden — the server never holds plaintext or a recovery key. If you forget the master password and have no emergency-access contact configured, the vault is gone. Configure emergency access on day one.

FAQ

Can I run this on a Raspberry Pi? Yes. A Pi Zero 2W with 64-bit Raspberry Pi OS Lite handles a family vault. The original Pi Zero (ARMv6) cannot run current Docker images. A Pi 4 or 5 is overkill but trivial.

How do I migrate from Bitwarden Cloud or 1Password? Export from the source manager (encrypted JSON for Bitwarden, .1pux for 1Password), then import via the Vaultwarden web vault under Tools → Import Data. SSH keys and passkeys carry across in recent client versions.

What happens when Vaultwarden falls behind Bitwarden’s API? Occasionally a Bitwarden client release ships ahead of the corresponding Vaultwarden compatibility update — the maintainers track this and usually catch up within a release cycle. Pinning the Bitwarden client version on critical devices is a reasonable hedge for a few weeks if a regression appears.

Is Vaultwarden affiliated with Bitwarden? No, though one active Vaultwarden maintainer is employed by Bitwarden and contributes independently. The project explicitly disclaims association and exists to serve the self-hosting community.

Worth the Hour

Vaultwarden is the rare self-hosted tool where the operational tax is genuinely small and the value is genuinely large. A working deployment is a Docker container, a Caddyfile, a cron job, and a restored-backup test. The hardest part is the discipline — patching on a schedule, watching the GitHub release feed, keeping the backup chain honest. Do that, and you have a credential store that no vendor can lose, brick, or price-hike out from under you. Start the timer.

Add a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Cybersecurity intelligence delivered directly to your inbox.

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Advertisement