Self-Hosting
Vaultwarden: secure setup & backup
Run Vaultwarden the operator way — HTTPS via a reverse proxy (clients refuse plain HTTP), the Argon2 admin token and its Compose $$ trap, a locked-down admin page, and a SQLite .backup that restores attachments too.
Problem summary
Vaultwarden (the lightweight Rust Bitwarden server) has two non-negotiables: it must be reached over HTTPS — Bitwarden clients refuse a non-TLS connection, and the project recommends terminating TLS at a reverse proxy rather than its built-in SSL — and the admin page must be locked down or disabled. Backup has one rule that matters: never raw-copy a live `db.sqlite3` (the `-wal`/`-shm` sidecars make that corrupt). Use SQLite's `.backup` or the built-in `vaultwarden backup`, and also copy the `config.json`, the RSA key, and the `attachments/` and `sends/` folders — restore the DB without `attachments/` and your files come back broken.
Confirm clients reach Vaultwarden over HTTPS, not http://.
docker exec -it vaultwarden /vaultwarden hash # produces $argon2id$v=19$...
The client connects over a trusted HTTPS URL.
DB-only backups restore with broken attachments.
Layer path
Step-by-step runbook
Start here. Do each check in order, compare it to the expected result, and stop when the evidence explains the failure or the safe stop point applies.
Put TLS in front
Check: Configure a reverse proxy to terminate HTTPS to Vaultwarden over HTTP.
Expected result: Clients connect over a trusted HTTPS URL.
If not: If clients still fail, the cert isn't trusted — fix the cert path.
Harden the admin token
Check: Generate an Argon2 hash and set ADMIN_TOKEN (escaping $ as $$ in Compose).
Expected result: No plaintext-token notice in the logs; admin login works.
If not: If login fails, check the $$ escaping and stray config.json token.
Lock down the admin page
Check: Restrict /admin to VPN/IP, or disable it entirely.
Expected result: The admin page isn't reachable from the internet.
If not: Remove ADMIN_TOKEN and config.json admin_token to disable fully.
Place data on local storage
Check: Ensure /data (the live SQLite DB) is on the local filesystem.
Expected result: No SQLite-over-network-share corruption risk.
If not: If it's on a share, migrate it local and back up to the share.
Back up the full set
Check: Use .backup / built-in backup, plus config.json, rsa_key, attachments/, sends/.
Expected result: A complete, consistent backup set exists.
If not: A 'readonly database' error means the backup user can't write /data.
Safe stop: DB-only backups restore with broken attachments.
Prove the restore
Check: Stop a scratch Vaultwarden, drop the files in, start, and log in.
Expected result: The vault and attachments come back; clients re-auth.
If not: Missing attachments mean the attachments/ folder wasn't restored.
Decision tree
If: Client error connecting
Then: TLS isn't satisfying the client.
Action: Put a reverse proxy in front on a trusted cert; don't rely on built-in SSL.
Safe stop: Stop exposing it on plain HTTP.
If: Admin login fails after hashing the token
Then: Compose mangled the $ characters, or a stale plaintext token remains.
Action: Escape every $ as $$ in Compose YAML and remove admin_token from config.json.
If: Backup script errors 'readonly database'
Then: The cron/backup user can't create the SQLite -wal file in /data.
Action: Grant write access to /data for the backup user.
If: Restored vault has missing/broken attachments
Then: Only the DB was restored.
Action: Restore attachments/ (and sends/) to their original locations too.
If: DB on a NAS share keeps corrupting
Then: SQLite over a network filesystem.
Action: Move db.sqlite3 to local storage; back up to the share instead.
Safe stop: Don't run the live SQLite DB on NFS/CIFS.
Evidence table
| Symptom | Evidence to collect | Likely layer | Next action |
|---|---|---|---|
| Bitwarden app/extension won't connect | Whether the URL is HTTPS and the cert is trusted | TLS not terminated correctly | Front Vaultwarden with a reverse proxy on a real cert. |
| Startup log shows the plaintext-token notice | The ADMIN_TOKEN value | Un-hashed admin token | Generate an Argon2 hash with `vaultwarden hash`. |
| Admin page rejects the correct password | The Compose YAML around ADMIN_TOKEN; config.json | Unescaped $ / leftover plaintext token | Escape $ as $$; remove admin_token from config.json. |
| Backup job fails or the DB is corrupt | Backup method (raw copy vs .backup) and /data permissions/medium | Raw-copying a live DB, no write access, or a network share | Use .backup / built-in backup; local storage; writable /data. |
| Restored vault loads but attachments error | Whether attachments/ was in the backup set | DB-only restore | Restore attachments/ and sends/ alongside the DB. |
Commands and settings paths
Hash the admin token
docker exec -it vaultwarden /vaultwarden hash # produces $argon2id$v=19$...
Where: On the Docker host
Expected: An Argon2 PHC string you place in ADMIN_TOKEN.
Failure means: If the command is missing, the image is too old for built-in hashing.
Safe next step: In Compose YAML, escape every $ as $$ when you paste the hash.
Back up the database safely
docker exec -it vaultwarden /vaultwarden backup # writes db_YYYYMMDD_HHMMSS.sqlite3 to /data
Where: On the Docker host
Expected: A timestamped, consistent SQLite backup file in /data.
Failure means: A 'readonly database' error means /data isn't writable by the process.
Safe next step: Then also copy config.json, rsa_key.pem, attachments/, sends/.
Verify clients are hitting HTTPS
curl -sI https://<your-vaultwarden-host>/alive
Where: From a client machine
Expected: A 200/HTTP response over TLS with a trusted chain.
Failure means: A cert error or connection refusal means TLS/proxy isn't right.
Safe next step: Fix the reverse proxy / certificate before onboarding clients.
Hardware and platform boundary
Change only when
- Add a reverse proxy the moment any client other than localhost needs to connect.
- Move to a scheduled backup (built-in or .backup) once the vault holds anything you can't recreate.
Evidence that matters
- HTTPS via a reverse proxy on a trusted certificate.
- An Argon2-hashed admin token (or a disabled admin page) and a non-internet-exposed /admin.
- A full backup set (DB + config.json + rsa_key + attachments/ + sends/) on local storage, restore-proven.
Evidence that does not matter
- The built-in SSL server for client-facing TLS — use a proxy.
- icon_cache/ in the backup — it regenerates.
Avoid
- Raw-copying a live db.sqlite3 (with -wal/-shm present).
- Running the SQLite DB on a network share.
- Exposing the admin page to the internet.
Related tool
Use the linked tool to turn this runbook into a guided check for your exact setup.
Backup plan builderRelated problems
Last reviewed
2026-06-03 · Reviewed by HomeTechOps. Built from 2026-06 research verified against the Vaultwarden wiki (backing up your vault), the built-in-backup discussion, the Argon2 admin-token guidance, and the HTTPS-via-reverse-proxy rationale. The operator differentiators are the never-raw-copy-SQLite rule, the Compose $$ escaping trap, and restoring attachments/ alongside the DB.
Sources/assumptions
- Assumes Vaultwarden in Docker with a /data volume on local storage (not a network share — NFS/CIFS is a documented SQLite-corruption cause), fronted by a reverse proxy (Caddy/nginx/Traefik) that terminates TLS.
- The admin-token Argon2 hashing and the built-in backup command reflect current Vaultwarden (Argon2 token standard since ~v1.28); the RSA key filename moved from rsa_key to rsa_key.pem in modern installs — back up whichever exists.
- Security posture is stated as classes to enforce (HTTPS-only, admin page disabled or VPN/IP-restricted), not a claim about a specific proxy or host.
Source-backed checks
HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.