SQLite 適合在 VPS 正式環境使用嗎?
了解 SQLite 在單一 VPS 上正式運作的適用範圍,並掌握 WAL、busy_timeout、Litestream 備份設定,以及單一 writer 與跨機器共用的限制。
在 VPS 上使用 SQLite 作為正式環境資料庫的時機
對大多數小型應用程式而言,在 VPS 的正式環境中使用 SQLite 是正確選擇,原因很簡單:單一機器上的單一程序將資料寫入單一檔案,不需要資料庫伺服器。不必監控 daemon、不必在防火牆開放連接埠、不必輪替密碼,也不必維持第二台機器運作。查詢是函式呼叫,而不是網路往返,因此一個執行 40 次查詢的頁面,實際上只需付出 40 次函式呼叫的成本。
代價很明確,也確實存在。SQLite 在整個資料庫檔案中一次只允許 1 個寫入者,而且檔案不能由 2 台機器共用。對於在單一 VPS 上執行單一應用程式的情境,這 2 項限制都沒有問題。但只要超出這種架構,這 2 項限制就會立即成為致命問題。本指南涵蓋讓 SQLite 在伺服器上安全運作的設定、使用 Litestream 進行持續備份,以及應該停止使用 SQLite 的時機。
先安裝命令列工具。以下操作都在 Ubuntu 24.04 上執行。
sudo apt update
sudo apt install -y sqlite3
sqlite3 --version此命令會輸出以 3. 開頭的版本資訊,後面接著建置日期與原始碼雜湊值。截至 2026 年 10 月,Ubuntu 24.04 提供 SQLite 3.45.1。你的應用程式可能不會使用這個二進位檔:大多數語言執行環境會內建自己的 SQLite 函式庫副本,而且通常是較新的版本。因此,在依賴近期功能前,請先確認資料庫驅動程式回報的版本。
為什麼首先要變更 WAL 模式
SQLite 預設使用 rollback journal。變更頁面前,它會將原始頁面複製到 -journal 檔案,再直接修改資料庫。為了安全執行這項操作,SQLite 會鎖定整個檔案的獨佔鎖定。因此,只要有寫入進行中,所有讀取操作都必須等待。在筆記型電腦上通常不會察覺。在 Web 伺服器上,任何寫入速度緩慢的操作,都會讓所有存取該資料庫的請求停滯。
WAL(write-ahead log)模式會反轉這個順序。寫入者會將新頁面附加到獨立的 -wal 檔案,主資料庫則保持不變。讀取者會依開始讀取時建立的 snapshot,持續讀取主檔案。因此,讀取者不會阻塞寫入者,寫入者也不會阻塞讀取者。之後,checkpoint 會將累積的 WAL 頁面複製回主資料庫。這項變更是讓 SQLite 能在 Web 應用程式後端穩定運作的主要原因。
啟用 WAL 模式並確認設定已持續生效
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"指令會輸出 wal。這項輸出不是裝飾資訊。PRAGMA journal_mode 會回傳資料庫實際使用的模式,因此若回應為 delete,表示變更失敗,資料庫仍在使用 rollback journal。
WAL 模式具有持久性。它是資料庫標頭中的旗標,而不是連線設定。因此,每個資料庫檔案只需執行一次,之後的所有連線都會繼承此設定,重新開機後也一樣。請使用新的連線加以確認。
sqlite3 ~/app/app.db "PRAGMA journal_mode;"現在建立資料表,並查看磁碟上出現的檔案。
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/現在共有 3 個檔案:app.db、app.db-wal 和 app.db-shm。-wal 檔案儲存尚未完成 checkpoint 的已提交頁面。-shm 檔案是共享記憶體索引,所有連線都會對應該檔案,以便對 WAL 的內容保持一致。這兩個檔案都屬於資料庫,不是暫存檔案。應用程式執行期間若只複製 app.db,取得的檔案會遺漏所有最近的提交。若刪除 app.db 而保留另外兩個檔案,SQLite 會將那些過期的 WAL 頁面套用至以該名稱建立的新檔案。這就是嘗試重設資料庫時,可能導致全新資料庫損毀的原因。
生產環境應用程式需要的連線設定
只有 journal_mode 會儲存在資料庫中。以下其他設定都以連線為單位,表示應用程式必須在每個開啟的連線上執行這些設定,包括 connection pool 在背景建立的每個連線。
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 會讓 SQLite 在回傳 database is locked 前,持續重試遭鎖定的資料庫,最長可達 5000 毫秒。預設值為 0,因此 SQLite 預設會在兩個寫入者第一次重疊時立即失敗。只設定這個值,就能排除大多數通常歸咎於 SQLite 的鎖定錯誤。
synchronous = NORMAL 是 WAL mode 中正確的設定,但其中的取捨值得了解。在 FULL 下,SQLite 每次 commit 都會對 WAL 呼叫 fsync。在 NORMAL 下,SQLite 會改在 checkpoint 時同步資料。SQLite 文件明確說明了因此放棄的特性:發生電源故障或硬重設後,交易不再具備 durability。資料庫不會因為斷電而損毀,但尚未寫入磁碟的最後幾筆 commit 會遺失。對 VPS 而言,這通常是合理的取捨,因為每次寫入都不必執行一次 fsync。
foreign_keys = ON 預設為關閉,以維持向後相容性,而且設定以連線為單位。即使 schema 中充滿 REFERENCES 子句,在每個連線啟用此設定前,也完全不會強制執行任何條件。
另一個設定只有在之後才會變得重要。WAL 大小超過 1000 個 pages 後,SQLite 會自動執行 checkpoint,而這項工作會由當下恰好完成交易的連線執行。單獨使用時這沒有問題。但在執行 Litestream 時,這就會成為需要考量的問題,因為 Litestream 需要控制 checkpoint 的執行時機。
設定 busy_timeout 後為何 database is locked 仍會發生
這是讓使用者改回 Postgres 的錯誤,而且原因很明確。
busy timeout 會安裝 busy handler,但 SQLite 不保證一定會呼叫它。
如果 SQLite 判定呼叫 busy handler 可能造成 deadlock,就會直接將 SQLITE_BUSY 傳回應用程式,而不呼叫 busy handler。
它要避免的 deadlock 發生在 transaction 升級時。在 SQLite 中,單獨的 BEGIN 表示 BEGIN DEFERRED。如果它後面的第一個 statement 是 SELECT,目前就是 read transaction。稍後同一個 transaction 中的 UPDATE 需要將 transaction 升級為 write transaction 時,如果另一個 connection 自 read transaction 開始後已經寫入資料,SQLite 就無法讓目前 connection 等待。這是因為目前的 snapshot 已經過期,繼續等待只會讓兩個 connection 彼此 deadlock。文件直接說明了結果:
後續的 write statement 會在可能的情況下將 transaction 升級為 write transaction,否則傳回 SQLITE_BUSY。
你的 5000 毫秒 timeout 根本不會被使用。錯誤會立即傳回,因此看起來就像設定沒有生效。
修正方式只有一個詞。
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE 會在一開始、讀取任何資料之前取得 write lock。這樣就不會發生升級,也沒有需要避免的 deadlock,因此 busy handler 會生效,connection 會等待輪到自己,而不是直接失敗。僅包含讀取操作的 transaction 應維持 deferred。任何包含寫入操作的 transaction 都應使用 immediate。
lock 錯誤的第二個原因更難察覺:讓 write transaction 跨越緩慢操作而長時間保持開啟。SQLite 會序列化 writers,因此如果 transaction 開啟後,透過 network 呼叫外部 API,最後才提交,這段呼叫的時間內會阻塞所有其他 writer。先讀取所需資料並關閉 transaction,執行緩慢操作,然後再開啟一個簡短的 write transaction 儲存結果。
使用 Litestream 持續備份
每晚複製一次最多會遺失 1 天的寫入內容,而對使用中的 SQLite 資料庫執行 cp,可能產生無法開啟的複本。以下兩種方式是安全的。sqlite3 app.db ".backup /path/to/backup.db" 使用 SQLite 的線上備份介面,可處理使用中的資料庫。Litestream 更進一步監看 WAL,持續將變更傳送至物件儲存,將最壞情況下的資料遺失量從 1 天降至約 1 秒。
Litestream 是一個與應用程式並行執行的 Go binary。它不會介於應用程式與資料庫之間。應用程式照常寫入 SQLite,而 Litestream 讀取 WAL 並上傳變更內容。
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截至 October 2026,官方 Linux 安裝頁面仍記載 v0.5.14。之後已有較新的 release:v0.5.15 於 21 July 2026 發布,而最新的 tag v0.5.17 於 31 August 2026 發布。請將兩行中的版本改成 releases page 上的目前 tag;如果 VPS 使用 arm64,請改用相符的 arm64 package。
設定檔位於 /etc/litestream.yml。先使用本機檔案 replica,因為不需要 cloud credentials,就能驗證完整流程。
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/app請注意,欄位是單數形式的 replica。Litestream 0.5 將 0.3 series 的 replicas array 改為單一 replica block,現在若設定檔包含兩個項目,啟動時會失敗。許多第三方指南仍顯示舊的 array,因此請依照上述結構複製,不要直接採用搜尋結果中的第一個範例。0.5 series 也將 litestream wal subcommand 改名為 litestream ltx,因為磁碟上的備份格式已變更。
啟用任何功能前,先確認設定檔可以解析。
sudo litestream databases -config /etc/litestream.yml接著手動驗證完整往返流程。此形式略過設定檔,將單一資料庫複製到單一路徑。
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app此命令會在前景執行並持續運作。在第二個 shell 中寫入一列資料,再將 replica 還原至新檔案。
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;"計數應包含新資料列。如果沒有,表示變更尚未同步:Litestream 會依據預設為 1 秒的 sync-interval 推送內容,因此請稍候再還原一次。這 1 秒也是你的復原點。若發生當機,最多會遺失上一次同步間隔內的寫入內容;任何設定都無法將此值降為 0。
實際使用時,請將 replica block 改為 S3 URL。這適用於 Amazon S3,也適用於其他供應商提供的 S3-compatible object storage。
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24h請勿將 credentials 寫入該檔案。Litestream 會從環境讀取 LITESTREAM_ACCESS_KEY_ID 與 LITESTREAM_SECRET_ACCESS_KEY,因此請將它們放在由 root 擁有且 mode 為 600 的 systemd drop-in 中。
上述 snapshot 值是預設值,而 retention 的預設值常令人意外。Retention 指 Litestream 保留 snapshots 及其相關檔案的時間,因此也決定你能還原到多早以前。24 小時表示,如果你在 Wednesday morning 發現錯誤的 migration,從 Monday 的狀態已經無法復原。在 Litestream 0.5 中,retention 是 root snapshot block 的全域設定,不是每個 replica 個別設定。若要保留 1 週,請將該 block 改為 retention: 168h,並承擔額外的儲存空間成本。在 replica block 中加入 retention: line 是 0.3 series 的語法。Litestream 0.5 會忽略這行而不顯示錯誤,並維持 24 小時的預設值。
在需要前先驗證還原
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;"給定資料庫路徑後,litestream restore 會在 /etc/litestream.yml 中查找相符的複本並下載。對於狀態正常的檔案,PRAGMA integrity_check 會輸出 ok;任何其他輸出都表示還原的複本無法使用。使用 systemd service 與 timer 排程執行此作業,並讀取輸出。在實際還原過一次備份前,無法確認備份是否可用。
在 systemd 下執行 Litestream
Debian 套件會安裝一個 litestream unit,該 unit 會讀取 /etc/litestream.yml。
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f正常輸出會依序列出設定檔中的每個資料庫,之後除了定期同步訊息外不會再輸出其他內容。若針對資料庫路徑出現 no such file or directory 錯誤,表示設定檔中的路徑錯誤,或執行程序無法讀取該路徑。此 unit 預設以 root 執行,權限高於這項工作所需。Litestream 必須能讀寫資料庫及其所在目錄,因為它會處理資料庫旁的 -wal 與 -shm 檔案。因此,請將執行身分設為應用程式目前使用的使用者帳號;以下範例使用 appuser。
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser使用 sudo systemctl daemon-reload 與 sudo systemctl restart litestream 套用設定。建立 具備最低權限的專用服務帳號只需幾分鐘,卻能讓伺服器上的備份代理程式與另一個 root 程序有所區別。
如果日後需要從零重建機器,還有一項啟動順序必須注意。應先還原資料庫,再啟動應用程式。litestream restore 接受 -if-db-not-exists;如果檔案已存在,該指令會以 0 結束,因此每次開機執行都安全。請將它放在應用程式 unit 的 ExecStartPre 行中。如此一來,全新的 VPS 會在應用程式啟動前下載資料庫,已存在資料庫的機器則不會執行任何動作。若希望集中在同一處管理,litestream replicate 也有對應的 -restore-if-db-not-exists 旗標。
VPS 上 SQLite 的限制
網路檔案系統。 這是無法透過設定避開的限制。WAL 模式要求所有使用資料庫的程序共用一小段記憶體,而 -shm 檔案就是用來提供這段記憶體的。SQLite 文件明確規定:
使用資料庫的所有程序都必須位於同一台主機電腦上;WAL 無法透過網路檔案系統運作。
因此,位於已掛載 NFS(網路檔案系統)或 SMB 共用上的資料庫可能損毀,而且沒有任何 pragma 能防止這種情況。這裡有一項常被忽略的差異。網路區塊裝置是多數 VPS 業者提供的額外儲存方式。Linux 會將它視為具有一般檔案系統的普通磁碟,這沒有問題。已掛載的檔案共用則不同。
第二台應用程式伺服器。 沒有任何設定能讓這種架構正常運作。一旦需要由兩台機器提供相同資料,就需要使用能透過網路通訊的資料庫。請在還有時間規劃時就決定是否進行這項遷移。
寫入量大的工作負載。 同一時間只能有一個寫入者,這是檔案格式的特性,不是可調整的參數。短時間寫入的成本很低,因為每次提交都是附加至 WAL,因此吞吐量與磁碟的小型寫入延遲關聯更密切,而不是與 CPU 關聯。請參閱 VPS 上的 NVMe 與 SATA SSD 儲存比較,了解這項差異的實際影響。真正的問題是長交易,因為它們會讓其他所有寫入者排在後面等待。
分析查詢。 SQLite 是為交易處理打造的列儲存。掃描一億列資料的儀表板屬於另一種工作,應使用另一種工具;DuckDB 與 SQLite 的伺服器工作比較說明了兩者的適用界線。
複寫期間的 VACUUM。 完整的 VACUUM 會重寫整個資料庫檔案,使 Litestream 所依賴的頁面層級追蹤失效。Litestream 文件也建議不要在複寫進行期間直接執行該操作。文件建議使用 VACUUM INTO,將壓縮後的副本寫入個別檔案,並保留原始檔案不變。若必須直接執行 vacuum,文件記載的程序比重新啟動更繁瑣:停止複寫器、執行 vacuum、刪除資料庫旁 Litestream 的隱藏追蹤目錄及現有的遠端副本資料,然後重新啟動複寫器,讓它從全新的完整快照開始。最後這個步驟會捨棄原本擁有的還原歷程。
同一個資料庫上執行兩個複寫器。 絕不要讓兩個 Litestream 程序同時處理相同的資料庫,或使用相同的副本目的地。文件明確指出,防止這種情況是你的責任;否則產生的副本將無法還原。
Litestream 不涵蓋的範圍
Litestream 只保護資料庫檔案,不處理其他內容。上傳的檔案、應用程式設定、TLS(傳輸層安全性)憑證及 unit 檔案仍需由您自行管理。搭配排程執行的使用 restic 進行加密的異機備份,即可涵蓋這兩個部分。如果這是新主機,新 VPS 上線後的前 10 分鐘會說明本指南假設已完成的使用者帳號與防火牆設定。
FAQ
SQLite 適合正式環境的應用程式嗎?
如果是在一台伺服器上執行一個應用程式,SQLite 足夠使用,但前提是啟用 WAL 模式、設定 busy timeout,並持續進行備份。真正重要的限制是架構上的:同一時間只能有一個寫入者,而且只能使用一台主機。符合這些限制的應用程式,可以使用不需經過網路、也不需要另外監控獨立程序的資料庫。不符合這些限制的應用程式需要 client-server 資料庫,任何調校都無法改變這項需求。
為什麼設定 busy_timeout 後,仍然會遇到 database is locked?
因為等待可能造成 deadlock 時,SQLite 會略過 busy handler。以單獨的 BEGIN 開始的 transaction 是 deferred transaction:開頭的 SELECT 會讓它進入 read transaction,之後的寫入則必須升級。如果另一個 connection 在此期間完成寫入,SQLite 會立即回傳 SQLITE_BUSY,而不呼叫你的 busy handler,因為你的 read snapshot 已經過時。凡是會執行寫入的 transaction,都應使用 BEGIN IMMEDIATE 開始,讓寫入鎖定在一開始就取得,並套用 timeout。
可以將 SQLite 資料庫放在網路儲存設備上嗎?
不可以放在 NFS 或 SMB 等 network filesystem 上。WAL 模式需要所有程序透過 -shm 檔案共用記憶體,而 SQLite 文件明確指出,所有使用該資料庫的程序都必須位於同一台主機。由供應商連接的 network block device 則不同:Linux 會將它視為具有一般 filesystem 的一般磁碟,SQLite 可在其上正常運作。
如果已經執行每晚備份,還需要 Litestream 嗎?
這取決於你能承受遺失多少資料。每晚執行一次的工作,代表最多可能遺失 24 小時的寫入內容。Litestream 約每秒同步一次,因此當機時大約只會遺失最後 1 秒的資料。使用 Litestream 也比透過 cp 複製資料庫檔案安全,因為後者可能在資料庫寫入期間擷取檔案。Litestream 只涵蓋資料庫,因此仍應同時執行一般檔案備份。