SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

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

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

Bản sao lưu Vaultwarden phải chứa gì

Bản sao lưu 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 database đang được ghi có thể tạo ra file không thể mở. Sau đó, giữ lại các file đi kèm database. Đây là phần nhiều người thường quên.

Khi cài đặt bằng Docker, thư mục dữ liệu là thư mục bạn đã mount vào /data. Đây 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: toàn bộ account, mọi mục trong vault, mọi folder và mọi organization. Mất file này là mất vault.
  • db.sqlite3-waldb.sqlite3-shm: write-ahead log (WAL) và chỉ mục shared memory của nó. Các thay đổi gần đây nằm ở đây cho đến khi SQLite gộp 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, nằm trong một thư mục 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.derrsa_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à thư mục duy nhất bạn có thể bỏ qua, vì Vaultwarden sẽ tải lại các icon này khi cần.

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

Hai lệnh sẽ trả lời câu hỏi đó. 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. Kết quả 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. Vì vậy, server lưu ciphertext mà nó không thể đọc. Prefix 2. là loại mã hóa của Bitwarden. Sau đó là initialization vector (IV), ciphertext và MAC (message authentication code), tất cả đều ở dạng base64 và được phân tách bằng |. Key dùng để giải mã được suy ra 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 nhau dù bạn chạy Vaultwarden hay server chính thức, như phần so sánh Vaultwarden và Bitwarden tự host giải thích.

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à recovery code của two-factor authentication được lưu ở dạng plain text, cùng với metadata như thời điểm tạo và organisation nào sở hữu item. Vì vậy, chính file backup cũng là một secret. 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 điều này quyết định các quy tắc lưu trữ ở phần dưới: bản sao được mã hóa trước khi rời khỏi server.

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

Theo mặc định, Vaultwarden chạy SQLite ở chế độ WAL (ENABLE_DB_WAL=true). Một thao tác ghi trước tiên được ghi vào db.sqlite3-wal, và chỉ khi checkpoint chạy thì dữ liệu mới được hợp nhất vào db.sqlite3. Nếu chỉ sao chép db.sqlite3, bạn sẽ nhận được database tại thời điểm checkpoint gần nhất. Vì vậy, một mật khẩu được lưu cách đây mười phút có thể không có trong bản lưu trữ mà không có cảnh báo nào.

Sao chép cả ba file bằng cp cũng không giải quyết được vấn đề. Các bản sao được tạo ở những thời điểm hơi khác nhau, nên WAL đã lưu có thể chứa các phiên bản page không còn khớp với file chính đã lưu. Sau đó SQLite khôi phục file này từ file kia và tạo ra kết quả sai. Bạn chỉ phát hiện ra sau đó rất lâu:

Error: database disk image is malformed

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

Dùng sqlite3 .backup để tạo bản sao cơ sở dữ liệu

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ó kết quả khác, bản sao không thể sử dụng được. 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.

Công cụ sqlite3 không nằm trong container Vaultwarden. Image được xây dựng trên debian:trixie-slim với ca-certificates, curl, libmariadb3, libpq5openssl, 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ỏ đến path đã mount. Các lệnh ở trên thực hiện đúng cách này. Nếu dữ liệu nằm trong named volume, docker volume inspect <name> sẽ in path trên host dưới /var/lib/docker/volumes/.

Vaultwarden cũng đã cung cấp lệnh backup riêng từ version 1.32.1. Trên server của bạn:

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 được tạo ngay 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ỉ hỗ trợ SQLite: với MariaDB hoặc PostgreSQL, lệnh sẽ dừng với The database type is not SQLite. Backups only works for SQLite databases.

Các tệp thường bị bỏ quên

attachments/ chứa ciphertext dưới các tên không có ý nghĩa. Bản ghi cơ sở dữ liệu của mỗi tệp đính kèm chứa tên tệp đã mã hóa và key material mà client cần để giải mã tệp. Tệp đính kèm không có trong cơ sở dữ liệu chỉ là dữ liệu rác không thể đọc, còn cơ sở dữ liệu không có tệp đính kèm sẽ tạo ra các mục mà người dùng tải xuống không thành công. Hãy sao lưu cả hai trong cùng một lần chạy.

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

rsa_key.pem ký các JSON web token (JWT) dùng để duy trì trạng thái đăng nhập của client. Nếu thiếu tệp 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 tồn tại vì được mã hóa bằng các key dẫn xuất từ master password. Khôi phục tệp key sẽ tránh việc đăng xuất hàng loạt.

sends/ chứa các tệp được liên kết với Send link. Thiếu các tệp này sẽ làm hỏng các lượt tải xuống đó, nhưng không ảnh hưởng đến phần nào khác.

Gộp 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 thành /usr/local/sbin/vw-backup.sh, dùng chmod 700 để cấp quyền thực thi, rồi chạy dưới quyền root. Dòng test thực hiện công việc quan trọng: sqlite3 vẫn trả về 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 một bản sao lỗi thành lỗi của script. Sau đó, set -euo pipefail dừng toàn bộ script, thay vì để tar tạo một archive hoàn chỉnh nhưng chứa database đã hỏng.

Lệnh tar -tzf cuối cùng liệt kê những gì bạn thực sự đã capture. Hãy đọc output này ngay lần đầu chạy. Bạn cần tìm ./db.sqlite3, ./rsa_key.pem, ./config.json./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 hằng đêm bằng systemd service và timer thay vì cron.

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

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ố lượng user phải khớp với số account bạn biết. Số lượng cipher phải gần với giá trị đang chạy lấy từ sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" và không bao giờ được bằng 0 đối với 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 một bản sao thứ hai 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-waldb.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 của database đó. 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

Chạy các lệnh nà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 user đó. Image mặc định chạy bằng 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ẽ không xử lý được bất kỳ request nào, và log sẽ ghi rõ nguyên nhân.

Khởi động thành công sẽ 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 thất bại, archive có database nhưng không có attachments/. Giữ lại data.old.* cho đến khi tất cả các bước kiểm tra đều đạt, 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 chiều 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.

Nơi không nên đặt bản backup

  • Không đặt trên cùng disk với thư mục dữ liệu. Chỉ một volume bị lỗi là mất cả hai bản copy, và một rm -rf trỏ sai đường dẫn cũng gây ra điều tương tự.
  • Không đặt trên cùng server, kể cả ở volume thứ hai. Kẻ tấn công đã chiếm được 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, tất cả đều 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 đáng dùng, nhưng chúng nằm trong cùng account với server, nên sự cố account cũng làm mất chúng.

Bản copy offsite là nơi restic phát huy tác dụng, vì restic mã hóa repository ngay 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 vào thư mục archive, không trỏ vào thư mục dữ liệu đang hoạt động, để dữ liệu upload là bản copy nhất quán mà bạn đã kiểm tra. Lưu mật khẩu repository ở nơi khác với server mà nó bảo vệ: nếu mất mật khẩu đó, 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, để việc server bị breach không thể xóa toàn bộ lịch sử backup của chính nó. Thiết lập backup restic trên VPS trình bày đầy đủ cách tạo repository và lịch chạy, còn So sánh restic với BorgBackup giúp bạn chọn công cụ nếu vẫn chưa quyết định.

Kiểm tra khôi phục theo lịch

Chọn một ngày mỗi tháng. Dùng restic restore latest --tag vaultwarden --target /tmp/vw-check để tải 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 khôi phục trong sáu tháng là bản backup có trạng thái không rõ. Bạn sẽ biết trạng thái của nó khi sự cố xảy ra, đây 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 đã khôi phục, rồi đăng nhập bằng một tài khoản 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. restic check --read-data-subset=10% theo cùng lịch sẽ 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 thay đổi gần đây nằm trong db.sqlite3-wal và chưa được ghi vào db.sqlite3. cp chỉ tệp chính sẽ âm thầm làm mất các thay đổi đó, còn sao chép hai tệp riêng biệt có thể tạo ra một cặp không đồng nhất và 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 dùng SQLite Online Backup API và tạo ra một tệp 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. Sao chép database an toàn trên server đang chạy. Attachments và Send files được ghi khi người dùng upload tệp, nên một tệp được thêm vào giữa lúc sao chép database và tar có thể không có trong archive của đêm đó. Trường hợp xấu nhất là bạn mất một attachment. Nếu vài giây downtime không thành vấn đề, hãy chạy docker compose stop trước script và docker compose start sau khi script kết thúc để loại bỏ cả rủi ro đó.

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

Vaultwarden tạo key mới khi khởi động. Key đó ký JSON web token (JWT) để duy trì session, nên mọi token hiện có sẽ không còn hợp lệ. Tất cả client sẽ bị logout và phải sign in lại. Nội dung vault không bị ảnh hưởng vì chúng được mã hóa bằng các key dẫn xuất 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 để không ai nhận ra quá trình 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à note là ciphertext, nhưng địa chỉ email, tên account, password hint và mã khôi phục two-factor là plain text trong database. Attacker có bản sao offline cũng có thể thử giải ciphertext theo tốc độ của họ. Hãy mã hóa archive trước khi đưa nó ra khỏi máy. restic repository tự thực hiện việc này, còn gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz tạo ra một tệp đã mã hóa duy nhất mà bạn có thể lưu trên 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à built-in command 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 tệp 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ó.