HomeTechOps

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.

Operator snapshotEvidence first
First proof

Confirm clients reach Vaultwarden over HTTPS, not http://.

Screen to open

docker exec -it vaultwarden /vaultwarden hash # produces $argon2id$v=19$...

Expected signal

The client connects over a trusted HTTPS URL.

Stop boundary

DB-only backups restore with broken attachments.

Layer path

1Vaultwarden has two hard requirements: it must be reached over HTTPS (Bitwarden clients refuse plain HTTP), and the admin page must be locked down or disabled. The project recommends terminating TLS at a reverse proxy rather than its built-in SSL.
2The /data directory is the whole server: db.sqlite3 (default), config.json, the RSA key (rsa_key.pem on modern installs), and the attachments/ and sends/ folders. icon_cache/ is regenerable.
3Backup has one correctness rule: never raw-copy a live SQLite DB — the -wal/-shm sidecars make that corrupt. Use SQLite's .backup or the built-in vaultwarden backup, and copy the non-DB files too.
4The admin token is Argon2-hashed now; a plaintext token logs an insecure-notice, and in Compose YAML the hash's $ characters must be escaped as $$.
Runbook

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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

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

Evidence table

SymptomEvidence to collectLikely layerNext action
Bitwarden app/extension won't connectWhether the URL is HTTPS and the cert is trustedTLS not terminated correctlyFront Vaultwarden with a reverse proxy on a real cert.
Startup log shows the plaintext-token noticeThe ADMIN_TOKEN valueUn-hashed admin tokenGenerate an Argon2 hash with `vaultwarden hash`.
Admin page rejects the correct passwordThe Compose YAML around ADMIN_TOKEN; config.jsonUnescaped $ / leftover plaintext tokenEscape $ as $$; remove admin_token from config.json.
Backup job fails or the DB is corruptBackup method (raw copy vs .backup) and /data permissions/mediumRaw-copying a live DB, no write access, or a network shareUse .backup / built-in backup; local storage; writable /data.
Restored vault loads but attachments errorWhether attachments/ was in the backup setDB-only restoreRestore attachments/ and sends/ alongside the DB.
Reference

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 boundary

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 builder

Related 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.