Dùng SQLite cho ứng dụng production trên VPS
SQLite là lựa chọn tối ưu cho ứng dụng nhỏ trên một VPS. Bài viết hướng dẫn cấu hình WAL mode, busy_timeout, cách dùng Litestream backup và giới hạn thực tế khi triển khai.
Khi 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 không cần database server. Bạn không cần giám sát daemon, mở firewall cho một port, xoay vòng password hay duy trì một máy thứ hai. 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ương ứng với 40 lần gọi hàm.
Giới hạn này là cụ thể 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. File cũng không thể được chia sẻ giữa hai máy. Cả hai giới hạn đều không thành vấn đề với một VPS chạy một ứng dụng duy nhất. Nhưng chúng sẽ trở thành giới hạn nghiêm trọng ngay khi mô hình triển khai của bạn lớn hơn như vậy. Hướng dẫn này trình bày các setting 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 mô hình này.
Trước tiên, hãy cài command line tool. Toàn bộ nội dung bên dưới được thực hiện trên Ubuntu 24.04.
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionLệnh này in ra version bắt đầu bằng 3., tiếp theo là ngày build và source hash. Tính đến tháng 10 năm 2026, Ubuntu 24.04 cung cấp SQLite 3.45.1. Ứng dụng của bạn có thể không 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 của bạn báo cáo trước khi sử dụng 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-wal và app.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.
Tại sao database is locked vẫn xảy ra sau khi bạn đã thiết lập 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 cam kết sẽ luôn gọi nó.
Nếu SQLite xác định rằng việc gọi busy handler có thể dẫn đến deadlock, nó sẽ trả về SQLITE_BUSY cho ứng dụng thay vì gọi busy handler.
Deadlock mà nó tránh xảy ra khi một transaction thực hiện nâng cấp quyền. Một lệnh BEGIN trầ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 lệnh UPDATE sau đó trong cùng transaction đó cần trở thành write transaction, và một kết nối khác đã ghi dữ liệu kể từ khi bạn bắt đầu đọc, SQLite không thể bắt bạn chờ đợi. Snapshot của bạn đã cũ và việc chờ đợi chỉ làm hai kết nối deadlock lẫn nhau. Tài liệu nêu rõ kết quả:
Các câu lệnh ghi tiếp theo sẽ nâng cấp transaction lên write transaction nếu có thể, hoặc trả về SQLITE_BUSY.
Timeout 5000 mili giây của bạn không bao giờ được sử dụng. Lỗi xuất hiện ngay lập tức, đó là lý do tại sao có vẻ như thiết lập này không có tác dụng.
Cách sửa chỉ gói gọn trong một từ.
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE lấy khóa ghi ngay từ đầu, trước khi đọc bất cứ thứ gì. Không có sự nâng cấp nào xảy ra, vì vậy không có deadlock để tránh, do đó busy handler sẽ hoạt động và kết nối sẽ chờ đến lượt thay vì bị lỗi. Hãy giữ các read-only transaction ở chế độ deferred. Bất kỳ transaction nào có chứa thao tác ghi nên được đặt là immediate.
Nguyên nhân thứ hai gây ra lỗi khóa khó phát hiện hơn: giữ một write transaction mở trong khi thực hiện các tác vụ chậm. SQLite tuần tự hóa các tiến trình ghi, vì vậy một transaction mở ra, gọi một API bên ngoài qua mạng, rồi mới commit sẽ chặn mọi writer khác trong suốt thời gian gọi API đó. Hãy đọc những gì bạn cần, đóng transaction, thực hiện tác vụ 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 chạy hằng đêm có thể làm mất tối đa 1 ngày dữ liệu ghi mới. Chạy cp trên một SQLite database đang hoạt động có thể tạo ra bản sao không mở được. Có 2 cách an toàn. sqlite3 app.db ".backup /path/to/backup.db" dùng online backup interface của SQLite nên có thể chạy trên database đang được sử dụng. Litestream tiến thêm một bước: nó theo dõi WAL và liên tục đẩy các thay đổi lên object storage. Nhờ đó, mức mất dữ liệu tối đa giảm từ 1 ngày xuống còn khoảng 1 giây.
Litestream là một Go binary chạy cùng application. Nó không nằm giữa app và database. Application 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 versionv0.5.14 vẫn là release được trang cài đặt Linux chính thức ghi nhận vào October 2026. Sau đó đã có các release mới: v0.5.15 vào 21 July 2026 và v0.5.17, tag mới nhất, vào 31 August 2026. Hãy đổi version trong cả 2 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 một local file replica. 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/appLưu ý field này là replica, ở dạng số ít. Litestream 0.5 đã thay array replicas của series 0.3 bằng một replica block duy nhất. Config có 2 entry hiện sẽ fail khi startup. Nhiều hướng dẫn bên thứ ba vẫn hiển thị array cũ, vì vậy hãy dùng đúng cấu trúc ở trên thay vì lấy ví dụ đầu tiên trong kết quả tìm kiếm. Series 0.5 cũng đổi tên subcommand litestream wal thành litestream ltx vì on-disk backup format đã thay đổi.
Hãy kiểm tra config có parse được trước khi enable bất kỳ thành phần nào.
sudo litestream databases -config /etc/litestream.ymlSau đó, hãy kiểm tra round trip bằng tay. Cách này bỏ qua config file và replicate 1 database vào 1 path.
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/appLệnh này chạy ở foreground và tiếp tục chạy. Trong shell thứ 2, hãy ghi 1 row rồi restore replica vào 1 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;"Count phải bao gồm row mới. Nếu không, thay đổi vẫn chưa được sync: Litestream push theo sync-interval, mặc định là 1 giây. Hãy chờ rồi restore lại. 1 giây đó cũng là recovery point của bạn. Khi crash, bạn mất tối đa các lần ghi trong sync interval gần nhất. Không có cấu hình nào đưa mức này về 0.
Đối với storage thực tế, hãy thay replica block bằng S3 URL. Cách này hoạt động với Amazon S3 và 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: 24hKhông lưu credentials trong file đó. Litestream đọc LITESTREAM_ACCESS_KEY_ID và LITESTREAM_SECRET_ACCESS_KEY từ environment. Vì vậy, hãy đặt chúng trong systemd drop-in do root sở hữu và đặt mode 600.
Các giá trị snapshot ở trên là mặc định. Giá trị retention thường gây bất ngờ. Retention là khoảng thời gian Litestream giữ snapshots và các file thuộc về chúng. Vì vậy, nó cũng quyết định bạn có thể restore lùi bao xa. 24 giờ có nghĩa là một migration lỗi mà bạn phát hiện vào sáng Wednesday đã không thể khôi phục từ state của Monday. Trong Litestream 0.5, retention là setting toàn cục trong root snapshot block, không phải setting riêng cho từng replica. Để giữ trong 1 tuần, hãy đổi block đó thành retention: 168h và chấp nhận phần storage tăng thêm. Một dòng retention: bên trong replica block là cú pháp của series 0.3. Litestream 0.5 bỏ qua dòng này mà không báo lỗi và vẫn dùng giá trị mặc định 24 giờ.
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 -fOutput bình thường liệt kê từng database trong config rồi không hiển thị gì ngoài 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 config không đúng hoặc process không thể đọc đường dẫn đó. Unit mặc định chạy dưới quyền root, cao hơn mức cần thiết cho công việc này. Litestream phải có quyền đọc và ghi cả database lẫn directory chứa database, vì nó làm việc với các file -wal và -shm nằm cạnh database. Vì vậy, hãy cho unit chạy bằng user account mà ứng dụng của bạn đang sử dụng, là appuser trong các ví dụ này.
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuserÁp dụng thay đổi bằng sudo systemctl daemon-reload và sudo systemctl restart litestream. Thiết lập một service account riêng với quyền tối thiểu chỉ mất vài phút. Đây là điểm khác biệt giữa một backup agent và một process thứ hai chạy dưới quyền root trên máy chủ.
Một chi tiết về thứ tự rất quan trọng khi bạn dựng lại máy từ đầu. Bạn cần restore database trước khi ứng dụng khởi động. litestream restore nhận -if-db-not-exists và trả về 0 khi file đã tồn tại, nên có thể chạy an toàn ở 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 đã có database sẽ không làm gì. litestream replicate cũng có flag -restore-if-db-not-exists tương ứng nếu bạn muốn quản lý thiết lập này tại một nơi duy nhất.
Khi SQLite không phù hợp trên VPS
Filesystem qua 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 này. Tài liệu SQLite nêu rõ quy tắc này:
Mọi process sử dụng database phải chạy trên cùng một máy chủ; WAL không hoạt động trên filesystem qua mạng.
Vì vậy, database trên NFS (network file system) hoặc SMB share được mount có thể bị hỏng, và không có pragma nào ngăn được việc này. Có một điểm khác biệt mà nhiều người bỏ qua. Một thiết bị block qua mạng, là loại storage mà hầu hết nhà cung cấp VPS gắn thêm, xuất hiện với Linux như một disk thông thường có filesystem thông thường trên đó. Trường hợp này không có vấn đề. Một file share được mount thì khác.
Máy chủ ứng dụng thứ hai. Không có setting nào làm mô hình này hoạt động an toàn. Khi cần 2 máy phục vụ cùng một data, 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 file format, 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 được append vào WAL. Do đó, throughput phụ thuộc vào latency của các lần ghi nhỏ trên disk nhiều hơn CPU. Xem NVMe so với SATA SSD storage trên VPS để biết 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 xếp hàng mọi writer khác 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 một trăm triệu row là công việc khác, cần một tool khác. DuckDB so với SQLite khi chạy trên 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. Việc này làm mất hiệu lực cơ chế theo dõi cấp page mà Litestream sử dụng. Tài liệu Litestream khuyến cáo không chạy thao tác này tại chỗ khi replication đang hoạt động. Tài liệu đề xuất dùng VACUUM INTO. Lệnh này ghi bản copy đã compact vào một file riêng và giữ nguyên file gốc. Nếu bắt buộc phải vacuum tại chỗ, quy trình được tài liệu hóa dài hơn việc restart: dừng replicator, chạy vacuum, xóa thư mục tracking ẩn của Litestream bên cạnh database và dữ liệu replica hiện có trên remote, sau đó khởi động lại để Litestream bắt đầu từ một full snapshot mới. Bước cuối này sẽ xóa toàn bộ lịch sử restore hiện có.
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ói rõ rằng bạn phải tự chịu trách nhiệm ngăn việc này xảy ra. Nếu không, replica tạo ra sẽ không thể restore.
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ó.