SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Backup và restore Vaultwarden trên VPS đúng cách

Dùng sqlite3 .backup để sao chép vault đang chạy, giữ attachments, config.json và rsa_key, rồi kiểm tra restore trước khi VPS gặp sự cố.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 5, 2026.

Một bản backup Vaultwarden phải chứa gì

Bản backup Vaultwarden là bản sao của toàn bộ thư mục dữ liệu. Database bên trong thư mục này phải được sao chép đúng cách. Hãy chạy sqlite3 db.sqlite3 ".backup out.sqlite3" thay vì cp, vì sao chép trực tiếp một database đang được ghi có thể tạo ra file không thể mở. Sau đó, giữ lại các file nằm cùng thư mục với database. Đây là phần nhiều người thường quên.

Trong cài đặt Docker, thư mục dữ liệu là thư mục bạn đã mount vào /data. Đó có thể là một path trên host hoặc một named volume. Sự khác nhau giữa bind mount và named volume quyết định vault thực sự nằm ở đâu trên disk. Thư mục này chứa các thành phần sau:

  • db.sqlite3: mọi account, mọi mục trong vault, mọi folder và mọi organisation. Mất file này là mất vault.
  • db.sqlite3-wal và db.sqlite3-shm: write-ahead log (WAL) và shared memory index của nó. Các thay đổi gần đây nằm ở đây cho đến khi SQLite ghi chúng vào file chính.
  • attachments/: các file người dùng đính kèm vào các mục trong vault, đã được mã hóa và nằm trong một directory riêng cho từng mục.
  • sends/: các file đứng sau các link Bitwarden Send.
  • config.json: mọi setting bạn đã lưu từ trang admin.
  • rsa_key.pem, cùng với rsa_key.der và rsa_key.pub.der trên các bản cài đặt cũ: key dùng để ký login token.
  • icon_cache/: các icon website đã tải xuống. Đây là directory duy nhất bạn có thể bỏ qua, vì Vaultwarden sẽ tải lại chúng khi cần.

Cơ sở dữ liệu Vaultwarden có an toàn không? File thực sự chứa gì

Hai lệnh có thể trả lời câu hỏi đó, và bạn có thể chạy cả hai ngay bây giờ.

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;"

Lệnh đầu tiên in địa chỉ email của người dùng ở dạng rõ. Lệnh thứ hai in tên của một item, có dạng như sau:

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

Tên item, username, password và ghi chú được client mã hóa trước khi gửi đi, nên server chỉ lưu ciphertext mà không thể đọc. Tiền tố 2. là loại mã hóa của Bitwarden, theo sau là vector khởi tạo (IV), ciphertext và MAC (message authentication code), tất cả ở dạng base64 và được phân tách bằng |. Key dùng để giải mã được tạo từ master password của tài khoản. Master password không bao giờ đến server ở dạng có thể sử dụng. Phần này giống hệt nhau dù bạn chạy Vaultwarden hay server chính thức, như phần so sánh Vaultwarden và Bitwarden self-hosted giải thích chi tiết.

Phần còn lại của database không được mã hóa. Địa chỉ email, tên tài khoản, gợi ý password và mã khôi phục two-factor được lưu dưới dạng plain text, cùng với metadata như thời điểm tạo và tổ chức sở hữu item. Vì vậy, chính file backup cũng là một bí mật. Bất kỳ ai có file này đều biết người dùng của bạn là ai và có thể tấn công các blob đã mã hóa offline với tốc độ phần cứng của họ cho phép. Chính yếu tố này quyết định các quy tắc lưu trữ ở phần dưới: bản sao phải được mã hóa trước khi rời server. Admin token là nửa còn lại của cùng vấn đề, và phần hardening cho Vaultwarden self-hosted xử lý cả hai.

Vì sao copy db.sqlite3 khi Vaultwarden đang chạy không phải là backup

Vaultwarden mặc định chạy SQLite ở chế độ WAL (ENABLE_DB_WAL=true). Dữ liệu ghi trước hết vào db.sqlite3-wal, rồi chỉ được hợp nhất vào db.sqlite3 khi có checkpoint. Chỉ copy riêng db.sqlite3 sẽ tạo ra database tại thời điểm checkpoint gần nhất. Vì vậy, một password được lưu từ 10 phút trước có thể không có trong archive mà không có cảnh báo nào.

Copy cả 3 file bằng cp cũng không giải quyết được vấn đề. Các file được copy ở những thời điểm hơi khác nhau. Vì vậy, WAL đã lưu có thể chứa các phiên bản page không còn khớp với main file đã lưu. SQLite sẽ khôi phục một file dựa trên file còn lại, nhưng kết quả bị sai. Bạn có thể chỉ phát hiện ra việc này rất lâu sau đó:

Error: database disk image is malformed

.backup tránh được vấn đề này vì nó sử dụng SQLite Online Backup API. SQLite tài liệu hóa API này là cách dùng để copy một database có thể đang được sử dụng. API đọc các page dưới read lock và bắt đầu lại nếu một writer thay đổi file trong lúc đó. Vì vậy, dữ liệu ghi xuống disk luôn thể hiện một thời điểm nhất quán.

Tạo bản sao database bằng 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;"

Lệnh cuối cùng in ok trên một dòng riêng. Nếu có nội dung khác, bản sao không thể sử dụng. Không giữ bản sao đó và không xóa bản sao trước đó. Toàn bộ chuỗi lệnh chạy trên server đang hoạt động, nên không ai bị đăng xuất và không có container nào bị restart.

Tool sqlite3 không nằm bên trong Vaultwarden container. Image được build trên debian:trixie-slim bằng ca-certificates, curl, libmariadb3, libpq5 và openssl, nên docker exec vaultwarden sqlite3 ... thất bại với lỗi:

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

Hãy chạy lệnh trên host và trỏ vào path đã mount. Đây là cách các lệnh ở trên hoạt động. Nếu dữ liệu nằm trong named volume, docker volume inspect <name> sẽ in host path bên dưới /var/lib/docker/volumes/.

Vaultwarden cũng đã cung cấp command backup riêng từ version 1.32.1. Trên server, chạy:

docker exec -it vaultwarden /vaultwarden backup

Lệnh này chạy VACUUM INTO và ghi db_YYYYMMDD_HHMMSS.sqlite3 vào data folder. Có 2 điểm cần lưu ý. Bản sao nằm cạnh bản gốc trên cùng một disk, nên đây mới chỉ là bước staging, chưa phải backup. Ngoài ra, lệnh này chỉ dùng được với SQLite: trên MariaDB hoặc PostgreSQL, lệnh sẽ dừng với lỗi The database type is not SQLite. Backups only works for SQLite databases.

Các file thường bị quên

attachments/ chứa ciphertext dưới các tên không có ý nghĩa. Mỗi attachment có một database row chứa tên file đã mã hóa và key material mà client cần để giải mã file. Attachment không có database sẽ chỉ là dữ liệu không thể đọc, còn database không có attachment sẽ tạo ra các item mà người dùng tải xuống bị lỗi. Hãy backup cả hai trong cùng một lần chạy.

config.json chứa mọi giá trị bạn đã lưu từ admin page. Các giá trị này được ưu tiên hơn environment variable tương ứng. Điều đó có thể gây lỗi theo cả hai chiều: khôi phục một config.json cũ sẽ âm thầm ghi đè các setting trong compose file, và bản thân file này là dữ liệu nhạy cảm vì có thể chứa SMTP password và admin token. Hãy lưu token dưới dạng chuỗi Argon2id PHC (password hashing competition), không lưu plain text. docker run --rm -it vaultwarden/server /vaultwarden hash sẽ tạo chuỗi này cho bạn.

rsa_key.pem dùng để ký JSON web token (JWT) giúp client duy trì trạng thái đăng nhập. Nếu thiếu file này khi khởi động, Vaultwarden sẽ tạo key mới. Khi đó, mọi token được ký bằng key cũ sẽ không còn được xác thực và tất cả client sẽ bị đăng xuất. Nội dung Vault vẫn được giữ nguyên vì chúng được mã hóa bằng các key dẫn xuất từ master password. Khôi phục file key sẽ tránh việc đăng xuất hàng loạt.

sends/ chứa các file được phục vụ qua Send link. Nếu thiếu file này, các lượt tải xuống đó sẽ lỗi, còn các chức năng khác không bị ảnh hưởng.

Đưa toàn bộ vào một 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"

Lưu script dưới tên /usr/local/sbin/vw-backup.sh, chmod 700, rồi chạy script với quyền root. Dòng test thực hiện công việc quan trọng: sqlite3 trả về mã 0 ngay cả khi PRAGMA integrity_check báo dữ liệu bị hỏng. Vì vậy, so sánh output với ok mới biến bản sao lỗi thành lỗi của script. Sau đó set -euo pipefail dừng mọi thứ, thay vì để tar tạo một archive hoàn chỉnh nhưng chứa database bị hỏng.

Lệnh tar -tzf cuối cùng liệt kê chính xác những gì bạn đã capture. Hãy đọc output này trong lần chạy đầu tiên. Bạn cần tìm ./db.sqlite3, ./rsa_key.pem, ./config.json và ./attachments/, đồng thời xác nhận không có ./db.sqlite3-wal. Nếu muốn có output journalctl và một unit báo lỗi, hãy chạy script mỗi đêm bằng systemd service và timer thay vì cron.

Xác minh bản backup bằng cách restore vào một thư mục tạm

Một bản backup chưa được kiểm thử chỉ là phỏng đoán. Restore vào một thư mục tạm mất khoảng một phút và không tác động đến dữ liệu đang chạy.

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

Có 4 kết quả cần kiểm tra. integrity_check in ra ok. Số user phải khớp với số account bạn biết. Số cipher phải gần với số liệu đang chạy từ sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" và không bao giờ được bằng 0 trên một vault đang được sử dụng. Thư mục attachments phải có kích thước gần với mức bạn dự kiến. Bạn có thể bỏ qua kiểm tra này nếu không có ai upload attachments. Sau đó chạy sudo rm -rf /tmp/vw-check, vì thư mục đó hiện chứa thêm một bản sao của mọi dữ liệu.

Khi restore bất kỳ thư mục dữ liệu nào được copy thủ công, luôn xóa db.sqlite3-wal và db.sqlite3-shm trước khi khởi động server. Nếu không, SQLite sẽ cố khôi phục database đã restore bằng một log thuộc về bản sao khác. Việc này làm hỏng database vốn được restore nguyên vẹn. Các archive do script ở trên tạo ra không bao giờ chứa những file đó, vì .backup ghi một database hoàn chỉnh.

Khôi phục trên server

Các lệnh này chạy trên server của bạn, khi container đã dừng. Vaultwarden không được ghi dữ liệu trong lúc thư mục dữ liệu bị thay đổi bên dưới nó.

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 phải chỉ định user mà container chạy bằng. Image mặc định chạy dưới root, vì vậy root:root là giá trị đúng trừ khi bạn đã đặt user: trong file compose. Khi đó, hãy dùng uid và gid tương ứng. Nếu server không thể ghi vào thư mục dữ liệu, trang đăng nhập sẽ xuất hiện nhưng mọi request đều fail. Log sẽ ghi rõ nguyên nhân.

Quá trình khởi động thành công kết thúc bằng dòng Rocket:

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

Sau đó, đăng nhập bằng trình duyệt, mở một item và tải xuống một attachment. Nếu đăng nhập được nhưng tải attachment fail, archive đã chứa database nhưng không chứa attachments/. Giữ lại data.old.* cho đến khi tất cả các bước kiểm tra đều thành công, rồi xóa nó. Rollback cũng gồm 3 bước tương tự, nhưng đổi vị trí các thư mục theo hướng ngược lại.

Nếu đường dẫn của bạn không khớp với các đường dẫn ở đây, hướng dẫn cài đặt Vaultwarden trên VPS có file compose mà các lệnh này giả định.

Không nên đặt backup ở đâu

  • Không đặt trên cùng disk với thư mục dữ liệu. Một volume hỏng sẽ làm mất cả hai bản sao. Một rm -rf đặt sai path cũng gây ra tình trạng tương tự.
  • Không đặt trên cùng server, kể cả trên volume thứ hai. Attacker có quyền root sẽ truy cập được backup trong cùng phiên.
  • Không đặt trong object storage nếu không mã hóa, vì archive chứa địa chỉ email, gợi ý mật khẩu, recovery code và ciphertext của vault, có thể bị tấn công offline.
  • Không chỉ dựa vào snapshot của provider. Snapshot khôi phục nhanh và vẫn rất hữu ích, nhưng chúng nằm trong cùng account với server. Sự cố account sẽ ảnh hưởng cả snapshot.

Bản sao offsite là nơi restic phù hợp, vì restic mã hóa repository trên máy trước khi upload bất kỳ dữ liệu nào. Trên server của bạn:

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

Trỏ restic đến thư mục archive, không trỏ đến thư mục dữ liệu đang hoạt động, để dữ liệu được upload là bản sao nhất quán mà bạn đã kiểm tra. Lưu password của repository ở nơi khác với server mà nó bảo vệ. Nếu mất password, các snapshot sẽ không thể đọc được theo đúng thiết kế. Nếu storage hỗ trợ, hãy cấp cho server credential có quyền ghi nhưng không có quyền xóa, để khi box bị compromise, attacker không thể xóa toàn bộ lịch sử của chính nó. Thiết lập restic backup trên VPS trình bày đầy đủ cách tạo repository và schedule. So sánh restic với BorgBackup trình bày cách lựa chọn nếu bạn chưa quyết định.

Kiểm tra restore theo lịch

Chọn một ngày mỗi tháng. Dùng restic restore latest --tag vaultwarden --target /tmp/vw-check để lấy snapshot mới nhất vào một thư mục tạm, chạy cùng PRAGMA integrity_check, chạy lại việc đếm số dòng, rồi ghi lại ngày và các số đếm. Một bản backup chưa từng được restore trong 6 tháng là bản backup không rõ trạng thái. Bạn sẽ chỉ biết trạng thái của nó khi xảy ra sự cố, tức là thời điểm tệ nhất để phát hiện vấn đề.

Mỗi năm một lần, thực hiện đầy đủ quy trình. Khởi động container Vaultwarden thứ hai trên một port dự phòng với thư mục dữ liệu đã restore, rồi đăng nhập bằng một account thật. Việc này kiểm chứng toàn bộ đường đi của master password, điều mà việc đếm số dòng không thể làm được. Chạy restic check --read-data-subset=10% theo cùng lịch để xác nhận dữ liệu đã lưu có thể đọc được, thay vì chỉ được liệt kê.

FAQ

Tôi có thể dùng cp để sao chép db.sqlite3 khi Vaultwarden đang chạy không?

Không. Vaultwarden chạy SQLite ở chế độ WAL, nên các bản ghi gần đây nằm trong db.sqlite3-wal và chưa có trong db.sqlite3. Chỉ sao chép file chính bằng cp sẽ âm thầm làm mất các bản ghi đó. Sao chép riêng từng file cũng có thể tạo ra một cặp file không khớp, về sau phát sinh Error: database disk image is malformed. Thay vào đó, hãy dùng sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'". Công cụ này sử dụng SQLite Online Backup API và tạo ra một file nhất quán trong khi server vẫn tiếp tục phục vụ.

Tôi có phải dừng container Vaultwarden để tạo backup không?

Không, và đó chính là mục đích của .backup. Có thể sao chép database an toàn khi server đang chạy. Attachments và file Send được ghi khi người dùng upload, nên file được thêm vào giữa lúc sao chép database và tar có thể không nằm trong archive của đêm đó. Trong trường hợp xấu nhất, bạn mất 1 attachment. Nếu vài giây downtime không thành vấn đề, chạy docker compose stop trước script và docker compose start sau đó sẽ loại bỏ cả rủi ro này.

Điều gì xảy ra nếu tôi restore mà không có các file rsa_key?

Vaultwarden tạo key mới khi khởi động. Key này ký các JSON web token (JWT) dùng để duy trì session, nên mọi token hiện có sẽ không còn được xác thực và tất cả client bị logout, sau đó phải đăng nhập lại. Nội dung vault không bị ảnh hưởng vì chúng được mã hóa bằng các key tạo từ master password của từng người dùng, không phải bằng RSA key. Hãy restore rsa_key.pem cùng với phần còn lại của data folder để người dùng không nhận ra việc restore.

Archive backup có an toàn để upload nguyên trạng lên object storage không?

Không. Tên item, password và ghi chú là ciphertext, nhưng địa chỉ email, tên account, gợi ý password và mã khôi phục two-factor là plain text trong database. Attacker offline có thể thử giải mã ciphertext theo tốc độ của họ. Hãy mã hóa archive trước khi đưa nó ra khỏi máy. Một repository của restic thực hiện việc này cho bạn, còn gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz tạo ra một file đã mã hóa duy nhất để bạn có thể chuyển cho bất kỳ storage nào.

Làm thế nào để backup Vaultwarden khi dùng PostgreSQL hoặc MariaDB?

Các bước dành cho SQLite không áp dụng được, và command tích hợp sẽ từ chối với The database type is not SQLite. Backups only works for SQLite databases. Hãy dump database bằng công cụ native tương ứng, pg_dump hoặc mysqldump, rồi giữ nguyên mọi quy tắc khác. Dump phải nằm trong cùng một archive với attachments/, sends/, config.json và các file rsa_key, được tạo trong cùng một lần chạy, mã hóa và lưu ở nơi khác với server đã tạo ra nó.