- SQLite trên 1 VPS đủ chạy 99% indie SaaS dưới 100k user, hiệu năng kinh ngạc.
- Litestream streams WAL của SQLite realtime ra S3/B2/R2, recovery point objective < 1 giây.
- Stack đơn giản nhất hành tinh: 1 VPS + 1 binary app + 1 file db + 1 binary litestream + 1 bucket.
- Pieter Levels style: không K8s, không Redis, không Postgres, không Docker phức tạp. Chỉ systemd + file.
- Khi vượt 100k user / 10GB db, mới cân nhắc Postgres + multi-server. Trước đó đơn giản là vũ khí.
Indie dev build SaaS một mình. Đọc trên Twitter: stack production nên là Postgres + Redis + RabbitMQ + Kubernetes + Elasticsearch. Bắt đầu setup 1 tuần, vẫn chưa code được dòng business logic. Trong khi đó, Pieter Levels (NomadList, RemoteOK, indie SaaS triệu đô) chỉ dùng PHP + MySQL + 1 VPS, không docker. Hiện tại có lựa chọn nhẹ hơn nữa: Go/Python + SQLite + Litestream + 1 VPS. Cấu hình tối thiểu, vận hành tự nhiên, đủ scale tới revenue đầu tiên.
Bài này hướng dẫn deploy 1 app full-stack với SQLite + Litestream backup realtime ra B2, kèm rationale khi nào nên (và không nên) chọn stack này. Tham khảo case study real từ Levels, Tailscale early days, và nhiều side project successful.
Vì sao SQLite OK cho production indie?
- Single file database, mỗi backup chỉ là copy 1 file. Đơn giản tới mức không có nhiều thứ để hỏng.
- Hiệu năng cực cao cho read-heavy: 100k+ read/giây trên 1 vCPU không cần tuning.
- WAL mode + busy_timeout đủ handle 500-1000 concurrent write/giây.
- Không cần network, không TCP overhead. App chạy cùng process với DB.
- Schema migration nhanh (ALTER TABLE đa số instant trên SQLite 3.35+).
- Không cần tuning shared_buffers, work_mem, max_connections - đỡ devops headache.
Hạn chế: không multi-writer cross-host (chỉ 1 process write/db file), không feature analytics nâng cao như Postgres (CTE, window function có nhưng cơ bản), full-text search có FTS5 nhưng kém Postgres GIN. Đa số indie SaaS không chạm hạn chế này.
Yêu cầu VPS
| Quy mô | RAM | vCPU | SSD |
|---|---|---|---|
| MVP launch, <1k user | 1 GB | 1 vCPU | 20 GB |
| 1k-10k user, app vừa | 2 GB | 2 vCPU | 40 GB |
| 10k-100k user | 4-8 GB | 4 vCPU | 80 GB |
| 100k+ và DB > 10 GB | 16+ GB | 4-8 vCPU | 200 GB+ |
VPS càng nhỏ càng cho thấy SQLite mạnh. App như Tailscale early days chạy SQLite trên 1 VPS phục vụ 10k+ user. Cost: vài USD/tháng cho gói 1-2 GB RAM.
Setup app + SQLite mẫu (Go)
// main.go
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3",
"file:/srv/app/app.db?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_foreign_keys=on")
if err != nil { log.Fatal(err) }
defer db.Close()
db.SetMaxOpenConns(1) // SQLite chỉ 1 writer
db.SetMaxIdleConns(1)
_, _ = db.Exec(`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
http.HandleFunc("/notes", func(w http.ResponseWriter, r *http.Request) {
// ... CRUD logic
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Quan trọng: _journal_mode=WAL bật Write-Ahead Logging, cho phép read đồng thời write. _busy_timeout=5000 đợi 5s khi lock thay vì error ngay. SetMaxOpenConns(1) chỉ 1 writer connection. Read song song qua nhiều connection riêng nếu cần throughput.
Cài Litestream
# Ubuntu / Debian
wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.deb
sudo dpkg -i litestream-v0.3.13-linux-amd64.deb
# AlmaLinux 9
wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.rpm
sudo rpm -i litestream-v0.3.13-linux-amd64.rpm
# Verify
litestream version
Cấu hình Litestream cho B2
Tạo file /etc/litestream.yml:
access-key-id: your-b2-keyID
secret-access-key: your-b2-applicationKey
dbs:
- path: /srv/app/app.db
replicas:
- name: b2-primary
url: s3://my-app-db.s3.us-west-002.backblazeb2.com/app
sync-interval: 1s
retention: 720h
retention-check-interval: 24h
snapshot-interval: 24h
validation-interval: 12h
sync-interval 1s nghĩa là WAL push lên B2 mỗi giây. Mất tối đa 1s dữ liệu khi disaster. retention 720h = 30 ngày. snapshot-interval 24h tạo full snapshot mỗi ngày để restore nhanh hơn.
Systemd service cho Litestream
# /etc/systemd/system/litestream.service đã có sẵn sau khi cài deb/rpm
sudo systemctl enable --now litestream
sudo systemctl status litestream
# Xem log
sudo journalctl -u litestream -f
Quan trọng: start litestream TRƯỚC khi start app. Litestream cần access trước app open WAL file để stream chính xác. Đặt thứ tự trong systemd: app.service có After=litestream.service.
Test restore
# Trên VPS mới hoặc folder test
litestream restore -o /tmp/restored.db s3://my-app-db.s3.us-west-002.backblazeb2.com/app
# Verify
sqlite3 /tmp/restored.db ".tables"
sqlite3 /tmp/restored.db "SELECT COUNT(*) FROM notes;"
# Restore tới 1 thời điểm cụ thể
litestream restore -timestamp 2026-05-22T10:00:00Z -o /tmp/restored.db s3://my-app-db.s3.us-west-002.backblazeb2.com/app
Point-in-time recovery là siêu năng lực. Lỡ xoá user nhầm lúc 9:55 sáng, restore tới 9:54 sáng, mất chỉ 1 phút dữ liệu nhưng cứu được hợp đồng.
Best practice production
- Đặt SQLite file trên SSD nhanh (NVMe), không trên network mount.
- Bật WAL mode + busy_timeout, set synchronous=NORMAL (không FULL) cho perf.
- VACUUM định kỳ tháng 1 lần qua cron khi traffic thấp.
- Index trên column hay query, EXPLAIN QUERY PLAN để verify.
- Connection pool: 1 writer + N reader. Trong Go thường ok với pattern 1 connection write, N connection read.
- App + DB cùng VPS, không qua network. Latency 10us thay vì 1ms của Postgres.
- Monitor litestream metric qua endpoint /metrics, scrape Prometheus.
Khi nào KHÔNG nên dùng SQLite?
- App yêu cầu multi-master write across nhiều VPS (vd geographic distribution).
- DB > 100 GB, query analytical complex, cần partitioning.
- Yêu cầu RPO = 0 (zero data loss), Litestream có lag 1s nên không zero.
- Cần feature Postgres riêng: PostGIS, JSON path, full-text Vietnamese, vector pgvector.
- Team backend có DBA experienced với Postgres, không có lợi thế dùng SQLite.
- App cần high write throughput > 5k write/giây sustained.
Migration sang Postgres khi cần
Khi tới ngưỡng, migrate SQLite -> Postgres không quá khó nếu code chuẩn:
- Export schema:
sqlite3 app.db .schema > schema.sql, sửa cú pháp cho Postgres (AUTOINCREMENT -> SERIAL). - Export data:
sqlite3 app.db .dump > data.sql, dùng pgloader hoặc script tay convert. - Đổi connection string app từ SQLite sang Postgres.
- Test thoroughly trên staging, đặc biệt query có syntax khác (LIKE case sensitivity, datetime func).
- Cutover production lúc low traffic.
Mất 1-2 tuần cho app trung. Code dùng ORM (sqlx, GORM, Drizzle) giảm đau đáng kể vì chỉ đổi driver.
Bẫy thường gặp
| Triệu chứng | Cách fix |
|---|---|
| "database is locked" | Bật WAL mode, set busy_timeout, giảm transaction dài |
| Litestream không stream | Litestream phải start trước app, kiểm tra permission file db |
| WAL file phình to | Bật autocheckpoint, default 1000 page (~4 MB) |
| Restore lỗi checksum | Litestream version bất đồng, dùng cùng version restore |
| Performance giảm dần | Cần VACUUM định kỳ, hoặc thiếu index |
VPS NVMe SSD cho SQLite production indie SaaS
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH performance cao, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Gói 2GB RAM đủ chạy 1 indie SaaS với SQLite + Litestream cho 10k user đầu.
Xem 8 cấu hình Cloud VPS →FAQ
SQLite có handle được 100 concurrent user không?
Có, dễ. SQLite trên WAL mode xử lý hàng nghìn concurrent read + vài trăm concurrent write trên 1 VPS 2 vCPU. Bottleneck thực tế là CPU cho business logic, không phải DB. Levels từng nói NomadList chạy SQLite phục vụ hơn 10k user/ngày trên 1 VPS nhỏ.
Litestream có replace được Postgres replication không?
Cho disaster recovery thì có. Cho read replica live thì gần như có (Litestream cũng có mode replicate sang VPS thứ 2 với độ trễ vài giây). Cho failover automatic master-replica như Postgres Patroni thì chưa - Litestream restore manual khi master chết. Phù hợp indie với RTO chấp nhận 5-10 phút downtime.
Có thể chạy nhiều SQLite trên 1 VPS không?
Có. Mỗi app/tenant 1 file db riêng. Litestream config nhiều dbs trong cùng yml. Hữu ích cho multi-tenant: 1 file db per khách hàng, isolation hoàn hảo, backup riêng. Sản phẩm như SimpleBackups, ExpressionEngine dùng pattern này.
SQLite có hỗ trợ JSON column không?
Có từ phiên bản 3.38. Lưu JSON dạng TEXT, query bằng JSON1 extension built-in: SELECT json_extract(data, '$.user.name'). Index trên expression giúp query nhanh: CREATE INDEX idx_user_name ON events(json_extract(data, '$.user.name')). Đủ dùng cho semi-structured data.
Nếu VPS bị xoá hoàn toàn, làm sao deploy lại nhanh?
1. Mua VPS mới. 2. Cài app binary + litestream. 3. litestream restore -o /srv/app/app.db s3://.... 4. Start app. Mất 5-15 phút tổng. Khác Postgres phải setup user, role, schema, restore dump - SQLite chỉ là 1 file. Đơn giản là sức mạnh.



