Vaultwarden¶
Overview¶
Vaultwarden is a lightweight, Rust re-implementation of the Bitwarden server API. The official Bitwarden apps, browser extensions and CLI talk to it as if it were Bitwarden, but it runs as one small container with SQLite instead of Bitwarden's MSSQL-based stack.
It holds every password I have, so it gets treated differently from the rest of the fleet:
- Pinned version, review-only updates. An unattended breaking or buggy release on the password vault is the last thing you want. The image tag is pinned, Renovate opens a PR for new versions, and Vaultwarden is on the manual-merge list: I read the release notes, then merge.
- No host port. The reverse proxy reaches the container over a shared Docker network; nothing else can.
- Signups off. Accounts are created by invite from the admin panel.
- Admin token as an Argon2 hash, not a plain-text token.
- Working SMTP, because invites, email verification and new-device alerts all depend on mail.
- Config in the environment, not the admin panel (see below).
Compose¶
services:
vaultwarden:
image: vaultwarden/server:1.37.4
container_name: vaultwarden
restart: unless-stopped
env_file:
- .env
environment:
DOMAIN: "https://vault.<YOUR_DOMAIN>"
SIGNUPS_ALLOWED: "false"
# Outbound mail (example: Gmail with an app password). SMTP_PASSWORD comes from .env.
SMTP_HOST: "smtp.gmail.com"
SMTP_PORT: "587"
SMTP_SECURITY: "starttls"
SMTP_FROM: "<YOUR_EMAIL>"
SMTP_FROM_NAME: "Vaultwarden"
SMTP_USERNAME: "<YOUR_EMAIL>"
# ADMIN_TOKEN comes from .env as an Argon2id PHC string.
# Keep config here, not in the admin panel: saving there writes data/config.json, which overrides env.
# No host port: the reverse proxy reaches vaultwarden:80 over the shared proxy network.
networks:
- tunnel-net
security_opt:
- no-new-privileges:true
volumes:
- /mnt/appdata/vaultwarden/data:/data
# The image ships its own HEALTHCHECK (curls /alive), so none is declared here.
networks:
tunnel-net:
external: true # created once with: docker network create tunnel-net
# Argon2id PHC string from `vaultwarden hash` (see below). Single quotes: no $ escaping needed.
ADMIN_TOKEN='<YOUR_ARGON2_PHC_STRING>'
# App password from your mail provider (not your account password)
SMTP_PASSWORD=<YOUR_SMTP_APP_PASSWORD>
tunnel-net is the network the reverse proxy (and, in my case, the Cloudflare tunnel's proxy hop) shares with the
apps it fronts. Because the container is only on that network, vaultwarden:80 is reachable from the proxy and from
nowhere else, not even the host's LAN address. Uptime monitoring then watches the container's health status instead
of an HTTP port.
Admin token as an Argon2 hash¶
A plain ADMIN_TOKEN is a password sitting in an env file. Vaultwarden can check a hash instead:
It asks for the password twice and prints a PHC string ($argon2id$v=19$...). Put that string in .env wrapped in
single quotes (as above). If you put it directly in the compose file's environment: block instead, every $ has
to be doubled to $$, or Compose tries to interpolate it. Run docker compose config to check what the container
will actually get.
Log in to /admin with the password, not the hash.
Don't click Save in the admin panel
The first time you save anything in /admin, Vaultwarden writes data/config.json, and values in that file
override the environment. From then on, changing compose.yaml silently does nothing for those keys. Use the
admin panel to look and test (e.g. SMTP Email Settings > Send test email works without saving), and keep the
real config in compose/.env where it's version-controlled.
SMTP¶
Without mail, an invite "succeeds" but nobody receives it, and verification and new-device emails never arrive. Any SMTP provider works. With Gmail:
- Turn on 2-Step Verification on the account.
- Create an app password at myaccount.google.com/apppasswords.
- Put it in
.envasSMTP_PASSWORD, set theSMTP_*block as above, redeploy. - Admin panel → SMTP Email Settings → Send test email (don't Save).
Don't deploy the SMTP_* settings before the password exists: with SMTP configured but unable to log in, invite and
verification flows fail outright instead of just skipping the mail.
First setup¶
- Deploy with
SIGNUPS_ALLOWED=falsefrom the start: nobody, including you, signs up through the public form. - Open
https://vault.<YOUR_DOMAIN>/admin, log in with the admin password, Users > Invite User with your address. - Accept the invite from the email, create the account and a strong master password, then turn on two-step login (TOTP or a passkey/security key) in the web vault.
- Import your old password manager's export (Tools > Import data), then delete the export file.
- In the Bitwarden apps/extensions, choose Self-hosted and enter your server URL.
Clients need to reach it
The mobile apps and browser extensions talk to the server directly. If you put the vault behind an SSO gateway (Cloudflare Access, Authelia, ...), the apps can't complete that login; either give the vault hostname its own policy (Vaultwarden has its own auth + 2FA) or only use it on the LAN/VPN.
Backups¶
/data holds the SQLite database, attachments, sends and the RSA keys. Back up the whole directory, and dump the
database with SQLite's own .backup (borgmatic has a sqlite_databases hook for this) rather than copying a live
db.sqlite3 mid-write. Test a restore at least once: a password vault you can't restore is worse than none.