Directions
1Start Vaultwarden with a data volume
Vaultwarden reimplements the Bitwarden server API in Rust — a fraction of the resources of the official server, which is why it's the homelab and small-team standard. One container, one volume holding the SQLite database and attachments.
Set an ADMIN_TOKEN (a long random string, stored in your existing password manager for the bootstrap irony) to enable the /admin panel where instance settings live.
docker run -d --restart=always \
-v vaultwarden:/data \
-e ADMIN_TOKEN=$(openssl rand -base64 48) \
-e SIGNUPS_ALLOWED=true \
-p 127.0.0.1:8080:80 --name vaultwarden \
vaultwarden/server:latest
2Put real TLS in front
Bitwarden clients require HTTPS — the web crypto they use won't run in an insecure context. Front the container with your reverse proxy (Traefik, Caddy, nginx) on a subdomain like vault.example.com with a Let's Encrypt certificate.
Decide exposure deliberately: internet-facing is convenient and fine with strong master passwords plus 2FA, but if the user set is just you or your household, serving it only over a private network (a tailnet) removes the whole class of internet-facing risk.
3Create accounts, then close signups
Open https://vault.example.com, create each user's account — the master password is the encryption key derivation input; it must be strong and is unrecoverable by design — then flip SIGNUPS_ALLOWED to false (or set SIGNUPS_DOMAINS_WHITELIST for your email domain) so strangers can't register on your instance.
Enable two-factor (TOTP) on every account from the web vault's security settings. For families and teams, create an Organization to share credential collections rather than exporting and pasting.
4Point the official apps at your server
Every official Bitwarden client works: browser extensions, mobile apps, desktop, CLI. On each login screen choose self-hosted and enter https://vault.example.com before signing in.
Sync is instant and offline access is built in — clients keep an encrypted local cache, so a server outage locks out changes, not reads. That property is what makes self-hosting this particular service reasonable.
5Automate encrypted backups
The /data volume is everything. Automate a nightly job that uses SQLite's backup command (not a raw copy of a live database file), tars the attachments and config alongside it, encrypts the result, and ships it off the machine — object storage, another box, anywhere that isn't the same disk.
Test a restore once: spin up a fresh container from a backup and log in. For credentials infrastructure, an untested backup is a hope, not a plan. Keep the image updated on a schedule too — watchtower or a monthly manual pull.
sqlite3 /data/db.sqlite3 ".backup /backups/db-$(date +%F).sqlite3"
Common issues & fixes
Browser extension says 'An error has occurred' on login.
The instance is served over plain HTTP or a bad certificate. Clients require valid HTTPS — fix the proxy/cert first, everything else follows.
Locked out of the admin panel.
ADMIN_TOKEN mismatch — it may be argon2-hashed in config. Reset it via environment variable and recreate the container; user vaults are unaffected.
Mobile app can't reach the tailnet-only server.
The phone isn't on the private network. Install the VPN client on the device, or expose the instance publicly and lean on 2FA.
Forgot a master password.
There is no recovery — that's the security model. Delete the account from the admin panel and re-create it; org-shared items survive with the organization.
Restored backup has a corrupted database.
The backup copied the SQLite file while it was mid-write. Switch the job to sqlite3 .backup (step five) which snapshots safely while live.
Honesty note: articles are drafted with AI assistance, then a human
checks each one against the vendor's current setup flow before it
publishes — nothing goes live unreviewed. Vendor interfaces still
change; if a step looks different, the underlying record is what
matters.