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

SQLite production trên VPS: Khi nào nên dùng

SQLite phù hợp cho app nhỏ trên một VPS. Thiết lập WAL, busy_timeout, backup bằng Litestream và nhận biết giới hạn một writer trước khi cần PostgreSQL.

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

Khi nào SQLite là database production phù hợp trên VPS

Chạy SQLite trong production trên VPS là lựa chọn phù hợp cho hầu hết ứng dụng nhỏ. Lý do rất đơn giản: một process trên một máy ghi vào một file thì không cần database server. Không có daemon để giám sát, không có port cần cấu hình trên firewall, không có password cần thay đổi định kỳ và không có máy thứ hai phải duy trì hoạt động. Một query là một lần gọi hàm thay vì một network round trip. Vì vậy, một page chạy 40 query sẽ tốn 40 lần gọi hàm.

Giới hạn này rõ ràng và có thật. SQLite chỉ cho phép một writer tại một thời điểm trên toàn bộ database file, và file đó không thể được share giữa hai máy. Cả hai giới hạn đều không đáng ngại với một VPS chạy một ứng dụng duy nhất. Nhưng chúng sẽ khiến hệ thống không thể tiếp tục hoạt động ngay khi bạn vượt ra ngoài mô hình đó. Hướng dẫn này trình bày các thiết lập giúp SQLite an toàn trên server, cách backup liên tục bằng Litestream và thời điểm bạn nên dừng sử dụng SQLite.

Trước tiên, hãy cài command line tool. Mọi lệnh bên dưới đều được chạy trên Ubuntu 24.04.

sudo apt update
sudo apt install -y sqlite3
sqlite3 --version

Lệnh này in ra một version bắt đầu bằng 3., tiếp theo là ngày build và source hash. Tính đến tháng 7 năm 2026, Ubuntu 24.04 phát hành SQLite 3.45.1. Ứng dụng của bạn có thể không sử dụng binary này: hầu hết language runtime đều đóng gói bản sao riêng của SQLite library, thường là bản mới hơn. Vì vậy, hãy kiểm tra version mà database driver báo cáo trước khi dựa vào một feature mới.

Vì sao WAL mode là thay đổi đầu tiên cần thực hiện

Theo mặc định, SQLite dùng rollback journal. Trước khi thay đổi một page, nó sao chép page gốc vào file -journal, rồi chỉnh sửa trực tiếp database. Để thực hiện an toàn, SQLite phải lấy exclusive lock trên toàn bộ file. Vì vậy, mọi reader đều phải chờ khi có bất kỳ thao tác ghi nào đang diễn ra. Trên laptop, bạn thường không nhận ra vấn đề này. Trên web server, một thao tác ghi chậm có thể làm trì hoãn mọi request truy cập database.

WAL (write-ahead log) mode đảo ngược thứ tự này. Writer ghi nối tiếp các page mới vào file -wal riêng và không thay đổi database chính. Reader tiếp tục đọc file chính tại snapshot mà nó đã bắt đầu, nên reader không chặn writer và writer cũng không chặn reader. Sau đó, một checkpoint sao chép các page đã tích lũy trong WAL trở lại database chính. Thay đổi này là yếu tố chính giúp SQLite hoạt động ổn định phía sau một web application.

Bật chế độ WAL và xác nhận chế độ đã được lưu

mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"

Lệnh in ra wal. Kết quả này không phải thông tin trang trí. PRAGMA journal_mode trả về chế độ mà database đang thực sự sử dụng, vì vậy phản hồi delete có nghĩa là thay đổi đã thất bại và database vẫn đang dùng rollback journal.

Chế độ WAL được lưu bền vững. Đây là một flag trong database header, không phải một thiết lập của connection. Vì vậy, bạn chỉ cần chạy lệnh này một lần cho mỗi database file. Mọi connection sau đó sẽ kế thừa chế độ này, kể cả sau khi reboot. Hãy xác nhận điều đó bằng một connection mới.

sqlite3 ~/app/app.db "PRAGMA journal_mode;"

Bây giờ hãy tạo một table và kiểm tra các file xuất hiện trên disk.

sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/

Lúc này có ba file: app.db, app.db-walapp.db-shm. File -wal chứa các page đã commit nhưng chưa được checkpoint. File -shm là shared memory index mà mọi connection đều map vào để thống nhất nội dung của WAL. Cả hai file đều thuộc về database và không phải scratch file. Nếu chỉ copy app.db trong khi application đang chạy, bạn sẽ nhận được một file thiếu mọi commit gần đây. Nếu xóa app.db nhưng giữ nguyên hai file còn lại, SQLite sẽ áp dụng các WAL page cũ đó vào file mới xuất hiện với tên này. Đây là cách người dùng làm hỏng một database mới khi cố gắng reset database cũ.

Các thiết lập kết nối mà mọi app production cần

Chỉ journal_mode được lưu trong database. Mọi thiết lập khác bên dưới áp dụng cho từng connection, nghĩa là application phải chạy thiết lập đó trên mỗi connection mà nó mở, bao gồm mọi connection do pool tạo ở chế độ nền.

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;

busy_timeout = 5000 yêu cầu SQLite tiếp tục thử truy cập database đang bị lock trong tối đa 5000 milliseconds trước khi trả về database is locked. Giá trị mặc định là 0, nên theo mặc định SQLite fail ngay lần đầu hai tiến trình ghi chồng lấn. Chỉ cần đặt một giá trị này là loại bỏ được phần lớn lỗi lock thường bị quy cho chính SQLite.

synchronous = NORMAL là thiết lập phù hợp khi chạy ở WAL mode, và cần hiểu rõ đánh đổi của nó. Với FULL, SQLite gọi fsync trên WAL ở mỗi lần commit. Với NORMAL, SQLite chỉ đồng bộ tại các checkpoint. Tài liệu SQLite nói rõ điều bạn phải đánh đổi: transaction không còn durable sau khi mất điện hoặc hard reset. Database không bị corrupt do sự cố mất điện đó; bạn chỉ mất các commit cuối cùng chưa được ghi xuống disk. Trên VPS, đây thường là đánh đổi phù hợp vì nó loại một lần fsync khỏi đường xử lý của mọi thao tác ghi.

foreign_keys = ON mặc định bị tắt để duy trì backward compatibility, và thiết lập này áp dụng cho từng connection. Một schema đầy các mệnh đề REFERENCES sẽ không enforce được gì cho đến khi từng connection bật thiết lập này.

Còn một thiết lập chỉ trở nên quan trọng ở giai đoạn sau. SQLite tự động thực hiện checkpoint khi WAL lớn hơn 1000 pages, và connection nào vừa hoàn tất transaction tại thời điểm đó sẽ thực hiện công việc này. Bản thân việc này không có vấn đề. Nhưng khi Litestream đang chạy, đây trở thành một vấn đề cần cân nhắc vì Litestream muốn kiểm soát thời điểm checkpoint diễn ra.

Vì sao database is locked vẫn xảy ra sau khi bạn đặt busy_timeout

Đây là lỗi khiến nhiều người quay lại dùng Postgres, và nó có một nguyên nhân cụ thể.

busy timeout cài đặt một busy handler, nhưng SQLite không đảm bảo sẽ gọi handler này.

Nếu SQLite xác định rằng việc gọi busy handler có thể gây deadlock, nó sẽ trả về SQLITE_BUSY cho ứng dụng thay vì gọi busy handler.

Deadlock mà SQLite tránh xảy ra khi transaction được nâng cấp. Một BEGIN không có tùy chọn trong SQLite có nghĩa là BEGIN DEFERRED. Nếu câu lệnh đầu tiên sau đó là SELECT, bạn đang ở trong một read transaction. Khi một UPDATE về sau trong cùng transaction đó cần chuyển thành write transaction, nhưng một connection khác đã ghi dữ liệu kể từ lúc bạn bắt đầu đọc, SQLite không thể bắt bạn chờ. Snapshot của bạn đã cũ, nên việc chờ chỉ khiến hai connection deadlock lẫn nhau. Tài liệu nêu kết quả trực tiếp:

Các câu lệnh ghi tiếp theo sẽ nâng cấp transaction thành write transaction nếu có thể, hoặc trả về SQLITE_BUSY.

Timeout 5000 millisecond của bạn không bao giờ được kiểm tra. Lỗi trả về ngay lập tức, nên bạn có cảm giác thiết lập này không có tác dụng.

Cách sửa chỉ gồm một từ.

BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;

BEGIN IMMEDIATE lấy write lock ngay từ đầu, trước khi đọc bất kỳ dữ liệu nào. Không có bước nâng cấp, nên không có deadlock cần tránh. Vì vậy busy handler được áp dụng, và connection sẽ chờ đến lượt thay vì lỗi. Giữ các read-only transaction ở chế độ deferred. Mọi transaction có chứa thao tác ghi phải dùng chế độ immediate.

Nguyên nhân thứ hai của lỗi lock khó nhận biết hơn: giữ write transaction mở trong khi thực hiện công việc chậm. SQLite tuần tự hóa các writer, nên một transaction mở, gọi external API qua network rồi mới commit sẽ chặn mọi writer khác trong suốt thời gian gọi đó. Hãy đọc dữ liệu cần thiết, đóng transaction, thực hiện công việc chậm, sau đó mở một write transaction ngắn để lưu kết quả.

Sao lưu liên tục với Litestream

Bản sao hằng đêm có thể làm mất tối đa một ngày dữ liệu ghi, còn chạy cp trên cơ sở dữ liệu SQLite đang hoạt động có thể tạo ra bản sao không mở được. Có hai cách an toàn. sqlite3 app.db ".backup /path/to/backup.db" sử dụng giao diện sao lưu trực tuyến của SQLite nên có thể chạy trên cơ sở dữ liệu đang được sử dụng. Litestream tiến thêm một bước: nó theo dõi WAL và liên tục gửi các thay đổi lên object storage. Nhờ đó, mức mất dữ liệu tối đa giảm từ một ngày xuống còn khoảng một giây.

Litestream là một binary Go chạy cùng máy với ứng dụng. Nó không nằm giữa ứng dụng và cơ sở dữ liệu. Ứng dụng vẫn ghi vào SQLite như trước, còn Litestream đọc WAL và upload phần đã thay đổi.

cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream version

v0.5.14 là bản release được trang cài đặt Linux chính thức ghi nhận vào tháng 7 năm 2026, còn v0.5.15 được phát hành sau đó vào ngày 21 tháng 7 năm 2026. Hãy thay phiên bản trong cả hai dòng để khớp với tag hiện tại trên trang releases. Nếu VPS của bạn dùng arm64, hãy chọn package arm64 tương ứng.

File cấu hình nằm tại /etc/litestream.yml. Hãy bắt đầu bằng replica cục bộ. Cách này kiểm tra được toàn bộ quy trình mà không cần cloud credentials.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      type: file
      path: /var/backups/litestream/app

Lưu ý rằng trường này là replica, ở dạng số ít. Litestream 0.5 đã thay array replicas của dòng 0.3 bằng một replica block duy nhất. File cấu hình chứa hai entry hiện sẽ fail khi khởi động. Nhiều hướng dẫn của bên thứ ba vẫn dùng array cũ, vì vậy hãy dùng đúng cấu trúc ở trên thay vì sao chép ví dụ đầu tiên tìm thấy trong kết quả tìm kiếm. Dòng 0.5 cũng đổi tên subcommand litestream wal thành litestream ltx vì format backup trên disk đã thay đổi.

Hãy kiểm tra cấu hình có parse được trước khi enable bất kỳ thứ gì.

sudo litestream databases -config /etc/litestream.yml

Sau đó, hãy kiểm tra toàn bộ vòng lặp theo cách thủ công. Dạng lệnh này bỏ qua file cấu hình và replicate một database đến một path.

mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app

Lệnh này chạy ở foreground và tiếp tục chạy. Trong shell thứ hai, hãy ghi một row rồi restore replica vào một file mới.

sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"

Kết quả đếm phải bao gồm row mới. Nếu không có, thay đổi chưa sync xong: Litestream push theo sync-interval, mặc định là 1 giây, nên hãy chờ rồi restore lại. Một giây đó cũng là recovery point của bạn. Khi có crash, tối đa bạn mất các lần ghi trong khoảng sync gần nhất. Không có cấu hình nào có thể giảm mức này xuống 0.

Khi dùng storage thực tế, hãy thay replica block bằng URL S3. Cách này hoạt động với Amazon S3 và cả object storage tương thích S3 của các provider khác.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      url: s3://your-bucket-name/app
      region: us-east-1

snapshot:
  interval: 24h
  retention: 24h

Không lưu credentials trong file đó. Litestream đọc LITESTREAM_ACCESS_KEY_IDLITESTREAM_SECRET_ACCESS_KEY từ environment, vì vậy hãy đặt chúng trong systemd drop-in do root sở hữu với mode 600.

Các giá trị snapshot ở trên là giá trị mặc định, còn retention mặc định thường gây bất ngờ. Retention là khoảng thời gian Litestream giữ các snapshot và các file liên quan, nên nó cũng quyết định bạn có thể restore lùi bao xa. Hai mươi bốn giờ nghĩa là một migration lỗi mà bạn phát hiện vào sáng thứ Tư đã không thể recover từ state của thứ Hai. Hãy đặt retention: 168h thành một tuần và chấp nhận phần storage tăng thêm.

Kiểm tra việc khôi phục trước khi cần dùng

litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"

Với đường dẫn cơ sở dữ liệu, litestream restore tìm replica tương ứng trong /etc/litestream.yml rồi tải replica đó xuống. PRAGMA integrity_check in ok khi file còn nguyên vẹn; mọi output khác đều có nghĩa là bản sao đã khôi phục không sử dụng được. Hãy chạy thao tác này theo lịch bằng một systemd service và timer, rồi đọc output. Cho đến khi bạn khôi phục một bản backup ít nhất một lần, bạn chưa thể biết nó có hoạt động hay không.

Chạy Litestream bằng systemd

Gói Debian cài một unit litestream đọc /etc/litestream.yml.

sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f

Output không có lỗi sẽ liệt kê từng database trong cấu hình, sau đó chỉ hiển thị các dòng đồng bộ định kỳ. Lỗi no such file or directory đối với đường dẫn database nghĩa là đường dẫn trong cấu hình bị sai hoặc tiến trình không thể đọc đường dẫn đó. Mặc định, unit chạy dưới quyền root, nhiều hơn mức công việc này cần. Litestream phải có quyền đọc và ghi cả database lẫn thư mục chứa database, vì nó làm việc với các file -wal-shm nằm cạnh database. Vì vậy, hãy chạy nó bằng account mà ứng dụng của bạn đang dùng.

# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser

Áp dụng cấu hình bằng sudo systemctl daemon-reloadsudo systemctl restart litestream. Việc tạo service account riêng với quyền tối thiểu chỉ mất vài phút. Đây là khác biệt giữa một backup agent và một tiến trình root thứ hai trên máy chủ.

Một chi tiết về thứ tự rất quan trọng nếu bạn phải dựng lại máy chủ từ đầu. Bạn cần restore database trước khi ứng dụng khởi động. litestream restore chấp nhận -if-db-not-exists; tùy chọn này thoát với mã 0 nếu file đã tồn tại, nên có thể chạy an toàn trong mỗi lần boot. Đặt lệnh này trong dòng ExecStartPre của unit ứng dụng. Khi đó, một VPS mới sẽ tải database xuống, còn máy chủ đã có database sẽ không làm gì. litestream replicate có flag -restore-if-db-not-exists tương ứng nếu bạn muốn đặt cấu hình này ở một nơi duy nhất.

SQLite không phù hợp trong những trường hợp nào trên VPS

Filesystem mạng. Đây là giới hạn không thể xử lý bằng cấu hình. Chế độ WAL yêu cầu mọi process sử dụng database phải chia sẻ một vùng memory nhỏ. File -shm cung cấp vùng memory này. Tài liệu SQLite nêu rõ quy tắc này:

Tất cả process sử dụng database phải chạy trên cùng một máy chủ; WAL không hoạt động qua filesystem mạng.

Vì vậy, database nằm trên NFS (network file system) hoặc share SMB đã mount có thể bị hỏng, và không có pragma nào ngăn được việc đó. Có một điểm khác biệt mà nhiều người bỏ qua. Thiết bị block mạng, là loại mà hầu hết nhà cung cấp VPS gắn thêm làm storage, xuất hiện trong Linux như một disk thông thường có filesystem thông thường. Trường hợp này không có vấn đề. Một file share đã mount thì khác.

Máy chủ ứng dụng thứ hai. Không có setting nào khiến mô hình này hoạt động an toàn. Khi cần 2 máy phục vụ cùng một bộ dữ liệu, bạn cần một database giao tiếp qua network. Hãy quyết định chuyển đổi khi vẫn còn thời gian để lập kế hoạch.

Workload ghi nhiều. Mỗi lần chỉ có một writer là đặc tính của định dạng file, không phải tham số có thể tinh chỉnh. Các lần ghi ngắn có chi phí thấp vì mỗi commit chỉ là một thao tác append vào WAL. Vì vậy, throughput phụ thuộc vào độ trễ ghi nhỏ của disk nhiều hơn CPU. Xem NVMe so với storage SATA SSD trên VPS để biết sự khác biệt này thể hiện như thế nào. Transaction dài mới là vấn đề thực sự, vì chúng khiến mọi writer khác phải xếp hàng phía sau.

Truy vấn phân tích. SQLite là row store được xây dựng cho transaction. Một dashboard quét 100 triệu row là một loại workload khác và cần một công cụ khác. DuckDB so với SQLite cho tác vụ server giải thích ranh giới này.

VACUUM khi replication. Một lần VACUUM đầy đủ sẽ ghi lại toàn bộ database file. Vì vậy, Litestream phải upload lại toàn bộ file, và tài liệu Litestream khuyến cáo không chạy thao tác này trong khi replication đang hoạt động. Hãy stop replicator, chạy vacuum, rồi start lại. Bạn nên chờ một full snapshot mới.

2 replicator trên cùng một database. Không bao giờ chạy 2 process Litestream trên cùng một database hoặc cùng một replica destination. Tài liệu nêu rõ việc ngăn chặn tình huống này là trách nhiệm của bạn. Nếu không, replica có thể không restore được.

Litestream không xử lý những gì

Litestream chỉ bảo vệ file database, không bảo vệ phần nào khác. Các file đã upload, cấu hình ứng dụng, certificate TLS (transport layer security) và các unit file vẫn do bạn tự quản lý. Hãy kết hợp Litestream với backup được mã hóa ngoài máy bằng restic theo lịch để bao quát cả hai phần. Nếu máy còn mới, mười phút đầu tiên trên một VPS mới sẽ hướng dẫn phần tạo user account và cấu hình firewall mà tutorial này giả định là đã hoàn tất.

FAQ

SQLite có đủ tốt cho một ứng dụng production không?

Với một ứng dụng trên một server, có, miễn là bạn bật chế độ WAL, đặt busy timeout và backup liên tục. Các giới hạn quan trọng nằm ở kiến trúc: mỗi lần chỉ có một writer và chỉ có một máy host. Ứng dụng nằm trong các giới hạn này sẽ có database không cần network hop và không cần process riêng để monitor. Nếu ứng dụng vượt quá các giới hạn đó, bạn cần database client-server; không cách tuning nào có thể thay đổi điều này.

Tại sao tôi vẫn gặp database is locked sau khi đặt busy_timeout?

Vì SQLite bỏ qua busy handler khi việc chờ có thể gây deadlock. Transaction bắt đầu bằng BEGIN không có tùy chọn là transaction deferred: một SELECT mở đầu sẽ đưa nó vào read transaction, rồi thao tác write sau đó phải nâng cấp transaction. Nếu một connection khác đã write trong khoảng thời gian đó, SQLite trả về SQLITE_BUSY ngay lập tức thay vì gọi busy handler, vì read snapshot của bạn đã stale. Hãy bắt đầu mọi transaction có thao tác write bằng BEGIN IMMEDIATE để lấy write lock ngay từ đầu và áp dụng timeout.

Tôi có thể lưu database SQLite trên network storage không?

Không nên lưu trên network filesystem như NFS hoặc SMB. Chế độ WAL cần mọi process chia sẻ memory thông qua file -shm, và tài liệu SQLite nêu rõ mọi process sử dụng database phải chạy trên cùng một host computer. Network block device do nhà cung cấp gắn vào là trường hợp khác: Linux nhìn thấy một disk thông thường với filesystem thông thường trên đó, nên SQLite hoạt động được.

Tôi có cần Litestream nếu đã chạy backup hằng đêm không?

Điều này phụ thuộc vào lượng dữ liệu bạn chấp nhận mất. Một job chạy hằng đêm có thể khiến bạn mất tối đa 24 giờ dữ liệu write. Litestream đồng bộ khoảng mỗi giây, nên khi crash bạn thường chỉ mất khoảng giây cuối cùng. Litestream cũng an toàn hơn việc copy database file bằng cp, vì cách đó có thể lấy phải database đang write dở. Litestream chỉ backup database, nên hãy chạy thêm một cơ chế backup file tổng quát bên cạnh nó.

#sqlite#wal#litestream#sao lưu#production