SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

Vaultwarden backup and restore for VPS wey go work

Use sqlite3 .backup for live Vaultwarden, save attachments, config.json and rsa_key files, then test restore so WAL files no spoil your backup.

Wetín Vaultwarden backup suppose contain

Vaultwarden backup na copy of the complete data folder, and you must copy the database wey dey inside am the correct way. Run sqlite3 db.sqlite3 ".backup out.sqlite3" instead of cp, because plain copy of database wey dey receive writes fit give you file wey no go open. Then keep the files wey dey beside am. Na this part people dey forget.

For Docker install, data folder na the one wey you mount for /data. E fit be path for host or named volume, and the difference between bind mounts and named volumes dey decide where your vault really dey for disk. Na wetin the folder contain be this.

  • db.sqlite3: every account, every vault item, every folder, and every organisation. If you lose this file, you lose the vault.
  • db.sqlite3-wal and db.sqlite3-shm: the write-ahead log (WAL) and the shared memory index. Recent writes dey here until SQLite merge dem into the main file.
  • attachments/: the files wey users attach to vault items, encrypted, with one directory for each item.
  • sends/: the files behind Bitwarden Send links.
  • config.json: every setting wey you save from the admin page.
  • rsa_key.pem, plus rsa_key.der and rsa_key.pub.der for older installs: the key wey dey sign login tokens.
  • icon_cache/: downloaded website icons. You fit skip this one directory, because Vaultwarden go fetch the icons again when e need dem.

My Vaultwarden database safe? Wetin the file really dey hold

Two commands fit answer that, and you fit run both now.

sudo apt update && sudo apt install -y sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select email from users;"
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select name from ciphers limit 1;"

The first one go print your users' email addresses as cleartext. The second one go print one item name, and e go look like this:

2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=

Client dey encrypt item names, usernames, passwords, and notes before e send dem, so server dey store ciphertext wey e no fit read. The 2. prefix na Bitwarden encryption type. E follow with initialisation vector (IV), ciphertext, and MAC (message authentication code). Dem all dey base64, and | separate dem. The key wey decrypt am dey derived from account master password, and that password never reach server in usable form. This part dey the same whether you dey run Vaultwarden or the official server, as the comparison between Vaultwarden and self-hosted Bitwarden explain.

The remaining part of database no dey encrypted. Email addresses, account names, password hints, and two-factor recovery codes dey stored as plain text, together with metadata like creation times and which organisation own an item. So the backup file itself na secret. Anybody wey get am go know who your users be, and fit attack the encrypted blobs offline at any speed their hardware allow. Na this fact make the storage rules wey follow important: encrypt the copy before e comot from the server.

Why copying db.sqlite3 while Vaultwarden runs no be backup

Vaultwarden dey run SQLite for WAL mode by default (ENABLE_DB_WAL=true). Write first dey enter db.sqlite3-wal, and na only checkpoint fit fold am into db.sqlite3. If you copy db.sqlite3 alone, you go get database as e be for the last checkpoint. So password wey person save ten minutes ago fit miss from your archive, and nothing go warn you.

Copying all three files with cp no solve the problem too. The copies happen for slightly different moments. So the WAL wey you save fit describe page versions wey no match the main file wey you save. SQLite go recover one from the other, and the result go wrong. You go discover am much later:

Error: database disk image is malformed

.backup avoid this because e dey use SQLite's Online Backup API. SQLite document am as the correct way to copy database wey fit dey active. E dey read the pages under read lock. If writer change the file while e dey read am, e go start again. So wetin finally land for disk represent one consistent moment.

Pega database copy na sqlite3 .backup

sudo apt update && sudo apt install -y sqlite3
sudo install -d -m 700 /var/backups/vaultwarden
OUT=/var/backups/vaultwarden/db-$(date '+%Y%m%d-%H%M').sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '$OUT'"
sudo sqlite3 "$OUT" "PRAGMA integrity_check;"

The last command dey print ok for one line by itself. Anything else mean say the copy no usable, so no keep am and no delete the previous one. The whole sequence dey run against live server, so nobody go log out and no container go restart.

The sqlite3 tool no dey inside Vaultwarden container. The image dey built on debian:trixie-slim with ca-certificates, curl, libmariadb3, libpq5 and openssl, so docker exec vaultwarden sqlite3 ... fail with:

exec: "sqlite3": executable file not found in $PATH

Run am for the host against the mounted path instead, na wetin the commands above dey do. If the data dey inside named volume, docker volume inspect <name> go print the host path under /var/lib/docker/volumes/.

Vaultwarden don also release its own backup command since version 1.32.1. For your server:

docker exec -it vaultwarden /vaultwarden backup

E dey run VACUUM INTO and write db_YYYYMMDD_HHMMSS.sqlite3 inside the data folder. Two things follow. The copy dey land beside the original for the same disk, so na staging step and e never be backup yet. And na SQLite only: for MariaDB or PostgreSQL, e stop with The database type is not SQLite. Backups only works for SQLite databases.

Dem files wey people dey forget

attachments/ dey hold ciphertext under names wey no clear meaning. Database row for each attachment get the encrypted file name and key material wey client need to decrypt the file. Attachments without database na unreadable noise, and database without the attachments go give users items wey their downloads no go work. Back up both for the same run.

config.json dey hold everything wey you save from the admin page, and the values for there get priority over matching environment variables. This fit cause problem both ways: restoring old config.json fit quietly override settings for your compose file, and the file itself dey sensitive because e fit contain your SMTP password and admin token. Store that token as an Argon2id PHC (password hashing competition) string instead of plain text. docker run --rm -it vaultwarden/server /vaultwarden hash go print one for you.

rsa_key.pem dey sign the JSON web tokens (JWT) wey keep clients logged in. If the file no dey when startup happen, Vaultwarden go generate new key. Because of that, every token wey the old key sign no go validate again, and all clients go log out. Vault contents still dey safe because keys wey come from the master password encrypt dem. Restoring the key file go prevent the mass logout.

sends/ dey hold the files behind Send links. If dem no dey, those downloads go break and nothing else.

Put everything inside one script

#!/bin/bash
set -euo pipefail

DATA=/opt/vaultwarden/data
DEST=/var/backups/vaultwarden
STAMP=$(date '+%Y%m%d-%H%M%S')
STAGE=$(mktemp -d /tmp/vw-stage.XXXXXX)

install -d -m 700 "$DEST"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
test "$(sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;')" = "ok"
cp -a "$DATA"/rsa_key* "$STAGE/"
for extra in config.json attachments sends; do
  if [ -e "$DATA/$extra" ]; then cp -a "$DATA/$extra" "$STAGE/"; fi
done
tar -C "$STAGE" -czf "$DEST/vw-$STAMP.tar.gz" .
chmod 600 "$DEST/vw-$STAMP.tar.gz"
rm -rf "$STAGE"
tar -tzf "$DEST/vw-$STAMP.tar.gz"

Save am as /usr/local/sbin/vw-backup.sh, chmod 700 am, and run am as root. The test line dey do real work: sqlite3 exits 0 even when PRAGMA integrity_check reports corruption, so comparing the output with ok na wetin make bad copy cause the script to fail. set -euo pipefail then stops everything, instead of allowing tar to build clean archive around broken database.

The final tar -tzf lists wetin you actually capture. Read am the first time. You dey look for ./db.sqlite3, ./rsa_key.pem, ./config.json and ./attachments/, and make ./db.sqlite3-wal no dey there. Run am every night with systemd service and timer instead of cron if you want journalctl output and a unit wey dey report failure.

Verify the backup by restoring am for one scratch directory

Backup wey nobody test na just guess. Restore am inside scratch directory; e go take one minute and e no go touch anything wey dey live.

sudo install -d -m 700 /tmp/vw-check
sudo tar -C /tmp/vw-check -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
ls -l /tmp/vw-check
sudo sqlite3 /tmp/vw-check/db.sqlite3 "PRAGMA integrity_check;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from users;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from ciphers;"
sudo du -sh /tmp/vw-check/attachments

Four results dey important. integrity_check go print ok. The user count go match the number of accounts wey you know. The cipher count go near the live figure from sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;", and e no suppose ever be zero for vault wey people dey use. The attachments directory suppose get roughly the size wey you expect. You fit skip this check if nobody dey upload attachments. Then run sudo rm -rf /tmp/vw-check, because that directory now get second copy of everything.

Follow this rule whenever you restore any data folder wey person copy by hand: delete db.sqlite3-wal and db.sqlite3-shm before you start the server. If you no do am, SQLite go try recover the restored database with log wey belong to another copy of the database. This fit corrupt database wey arrive intact. Archives wey the script above produce no dey contain those files, because .backup writes one complete database.

Restore for the server

Run these commands for your own server, while the container dey stopped. Vaultwarden no suppose dey write data while the data folder dey change under am.

cd /opt/vaultwarden
docker compose stop vaultwarden
sudo mv data data.old.$(date '+%Y%m%d-%H%M%S')
sudo install -d -m 700 data
sudo tar -C data -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
sudo chown -R root:root data
docker compose start vaultwarden
docker compose logs --tail 20 vaultwarden

chown suppose name the user wey the container dey run as. The stock image dey run as root, so root:root correct unless you set user: for your compose file. If so, use that uid and gid. If the server no fit write to the data folder, login page go fail every request, and the logs go show the reason.

A healthy start go end with the Rocket line:

[INFO] Rocket has launched from http://0.0.0.0:80

Then log in through browser, open one item, and download one attachment. If login work but attachment download fail, e mean say archive carry the database but e no carry attachments/. Keep data.old.* until everything check out, then delete am. To roll back, follow the same three steps but swap the directories the other way.

If your paths no match the ones for here, the Vaultwarden install guide for a VPS show the compose file wey these commands dey assume.

Wey you no suppose put backup

  • No put am for the same disk with the data folder. One failed volume go carry both copies, and one rm -rf for wrong path go do the same thing.
  • No put am for the same server, even if na for second volume. Attacker wey reach root fit reach your backups for the same session.
  • No put am for object storage without encryption, because the archive get email addresses, password hints, recovery codes, and vault ciphertext wey attacker fit attack offline.
  • No depend only on your provider's snapshots. Dem dey restore fast, and e good make you get dem, but dem dey for the same account with the server. So if account problem happen, e go affect dem too.

Offsite copy na where restic useful, because restic repository dey encrypted for the machine before anything upload. For your server:

sudo apt install -y restic
export RESTIC_REPOSITORY=s3:https://s3.example.com/vaultwarden-backups
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /var/backups/vaultwarden --tag vaultwarden
restic snapshots --tag vaultwarden
restic forget --tag vaultwarden --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Point restic to the archive directory, no be the live data folder, so wetin e upload na the consistent copy wey you don already check. Keep the repository password for another place, no be the server wey e dey protect. If you lose that password, the snapshots no go readable, by design. Where the storage support am, give the server credentials wey fit write but no fit delete. That way, if attacker breach the box, e no fit wipe its own history. How to set up restic backups for VPS cover the repository and schedule fully, while restic compared with BorgBackup explain the choice if you never decide yet.

Test restore on a schedule

Pick one day every month. Pull the newest snapshot enter a scratch directory with restic restore latest --tag vaultwarden --target /tmp/vw-check, run the same PRAGMA integrity_check, run the same row counts, then write the date and counts down. Backup wey nobody don restore for six months na backup with unknown state. You go only learn the state during outage, and that na the worst time to discover am.

Once every year, do the complete version. Start another Vaultwarden container for a spare port with the restored data folder, then log in with a real account. This one prove the master password path from start to finish, something no row count fit do. restic check --read-data-subset=10% for the same schedule go confirm say the stored data readable, instead of say e only dey listed.

FAQ

I fit use cp copy db.sqlite3 while Vaultwarden dey run?

No. Vaultwarden dey run SQLite for WAL mode, so recent writes dey inside db.sqlite3-wal and dem never enter db.sqlite3 yet. cp of only the main file go lose dem quietly, and if you copy the two files separately, e fit leave mismatched pair wey go later show as Error: database disk image is malformed. Use sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'" instead. E dey use SQLite Online Backup API and produce one consistent file while the server still dey serve requests.

I must stop Vaultwarden container before I take backup?

No, and na the reason for .backup. Database copy safe when server dey run. Attachments and Send files dey write when user upload one, so file wey dem add between database copy and tar fit miss that night archive. For worst case, na one attachment you go lose. If few seconds downtime no be problem, run docker compose stop before the script and docker compose start after am. This remove even that risk.

Wetin go happen if I restore without the rsa_key files?

Vaultwarden go generate new key when e start. That key dey sign JSON web tokens (JWT) wey keep sessions alive, so every existing token go stop to validate. All clients go log out and users must sign in again. Vault contents no dey affected, because keys wey come from each user's master password dey encrypt dem, not the RSA key. Restore rsa_key.pem together with the remaining data folder, and nobody go notice the restore.

Backup archive safe to upload to object storage as e be?

No. Item names, passwords, and notes na ciphertext, but email addresses, account names, password hints, and two-factor recovery codes dey plain text inside the database. Offline attacker fit also grind the ciphertext at their own pace. Encrypt the archive before e leave the machine. restic repository dey do this for you, while gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz dey produce one encrypted file wey you fit give any storage.

How I go back up Vaultwarden for PostgreSQL or MariaDB?

The SQLite steps no apply, and the built-in command dey refuse with The database type is not SQLite. Backups only works for SQLite databases. Dump the database with its native tool, pg_dump or mysqldump, and keep every other rule the same. The dump must enter one archive together with attachments/, sends/, config.json, and the rsa_key files. Take everything for the same run, encrypt am, and store am somewhere different from the server wey create am.