VPSでSQLiteを本番運用する方法と限界
1台のVPSに小規模アプリを構築するならSQLiteは有力です。WAL、busy_timeout、Litestreamによるバックアップ設定と、同時書き込み1つなどの限界を解説します。
VPS で SQLite が本番データベースに適している場合
VPS 上で SQLite を本番環境に使用することは、多くの小規模アプリケーションにとって適切な選択です。理由は単純です。1 台のマシン上で 1 つのプロセスが 1 つのファイルに書き込むだけなら、データベースサーバーは必要ありません。監視するデーモンも、ファイアウォールで保護するポートも、定期的に変更するパスワードも、稼働を維持する 2 台目のマシンも不要です。クエリはネットワーク往復ではなく関数呼び出しになるため、40 個のクエリを実行するページのコストは 40 回の関数呼び出しです。
制約は限定的ですが、明確に存在します。SQLite では、データベースファイル全体に対して同時に書き込めるライターは 1 つだけです。また、ファイルを 2 台のマシン間で共有することもできません。単一の VPS で単一のアプリケーションを実行する場合、どちらの制約も問題ありません。しかし、その構成を超えた瞬間に、どちらも致命的な制約になります。このガイドでは、SQLite をサーバー上で安全に運用するための設定、Litestream による継続的なバックアップ、そして SQLite の使用を停止すべきタイミングについて説明します。
最初にコマンドラインツールをインストールします。以下はすべて Ubuntu 24.04 で実行しています。
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionこのコマンドは、3.で始まり、ビルド日とソースハッシュが続くバージョンを出力します。2026 年 7 月時点で、Ubuntu 24.04 に含まれる SQLite は 3.45.1 です。アプリケーションがこのバイナリを使用しているとは限りません。多くの言語ランタイムは SQLite ライブラリを独自に組み込んでおり、そのバージョンはより新しいこともあります。そのため、新しい機能を利用する前に、データベースドライバーが報告するバージョンを確認してください。
WAL モードを最初に変更する理由
デフォルトでは、SQLite はロールバックジャーナルを使用します。ページを変更する前に、元のページを -journal ファイルへコピーし、その後データベースを直接編集します。これを安全に行うにはファイル全体を排他的にロックする必要があるため、書き込み中はすべての読み取り処理が待機します。ノートパソコンでは問題になりません。しかし Web サーバーでは、1 回の遅い書き込みによって、データベースにアクセスするすべてのリクエストが滞留します。
WAL(write-ahead log)モードでは、処理の順序が逆になります。書き込み処理は新しいページを別の -wal ファイルへ追加し、メインデータベースは変更しません。読み取り処理は開始時点のスナップショットに基づいてメインファイルを読み続けるため、読み取り処理が書き込み処理をブロックせず、書き込み処理も読み取り処理をブロックしません。後でチェックポイント処理が、WAL に蓄積されたページをメインデータベースへコピーします。この変更だけで、SQLite は Web アプリケーションの背後で実用的に運用できるようになります。
WAL モードを有効にし、設定が保持されていることを確認する
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"コマンドは wal を出力します。この出力は装飾ではありません。PRAGMA journal_mode はデータベースが実際に使用しているモードを返すため、delete という応答は変更に失敗し、まだロールバックジャーナルを使用していることを示します。
WAL モードは永続的です。これは接続ごとの設定ではなく、データベースヘッダー内のフラグです。そのため、データベースファイルごとに 1 回実行すれば、再起動後も含め、それ以降のすべての接続で引き継がれます。新しい接続で確認します。
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 ファイルには、まだチェックポイント処理されていないコミット済みページが格納されます。-shm ファイルは共有メモリのインデックスです。すべての接続がこれをマッピングし、WAL の内容について同じ状態を共有します。どちらもデータベースに属するファイルであり、一時ファイルではありません。アプリケーションの実行中に app.db だけをコピーすると、直近のコミットがすべて欠落したファイルになります。app.db を削除して残り 2 つのファイルを残すと、SQLite は古い WAL ページをその名前で新たに作成されたファイルへ適用します。このため、データベースをリセットしようとして新しいデータベースを破損させることがあります。
本番アプリケーションに必要な接続設定
データベースに保存されるのは journal_mode だけです。以下の他の設定はすべて接続ごとに適用されます。つまり、アプリケーションが開く各接続で実行する必要があります。バックグラウンドでプールが作成する接続も対象です。
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 は、ロックされたデータベースに対して最大 5000 ミリ秒間再試行してから database is locked を返すよう SQLite に指示します。デフォルト値は 0 です。そのため、デフォルトでは 2 つの書き込みが重なった最初の時点で SQLite は直ちに失敗します。この値を 1 つ設定するだけで、SQLite 自体の問題として扱われがちなロックエラーの大半を解消できます。
synchronous = NORMAL は WAL モードで適切な設定です。ただし、そのトレードオフは理解しておく必要があります。FULL では、SQLite はコミットのたびに WAL に対して fsync を呼び出します。NORMAL では、チェックポイントの時点で同期します。SQLite のドキュメントは、失われるものを明確に説明しています。電源障害や強制リセットの後、トランザクションは永続化されなくなります。ただし、その電源障害によってデータベースが破損することはありません。ディスクに到達していなかった最後のコミットが失われるだけです。VPS では通常、これが適切なトレードオフです。すべての書き込み処理から fsync を外せるためです。
foreign_keys = ON は後方互換性のためデフォルトで無効になっており、接続ごとに設定されます。スキーマ全体に REFERENCES 句があっても、各接続でこれを有効にするまでは何も強制されません。
後で重要になる設定がもう 1 つあります。SQLite は WAL が 1000 ページを超えると自動的にチェックポイントを実行します。この処理は、その時点でトランザクションを完了した接続が担当します。これ自体は問題ありません。Litestream を実行している場合は事情が変わります。Litestream はチェックポイントのタイミングを制御する必要があるためです。
busy_timeout を設定しても database is locked が発生する理由
これは Postgres に戻るきっかけになりやすいエラーで、原因は 1 つに絞れます。
busy timeout は busy handler を登録しますが、SQLite はそれを呼び出すとは限りません。
SQLite が busy handler の呼び出しによってデッドロックが発生する可能性があると判断した場合、busy handler を呼び出さず、代わりにアプリケーションへ SQLITE_BUSY を返します。
回避されるデッドロックは、トランザクションの昇格時に発生します。SQLite の単独の BEGIN は BEGIN DEFERRED を意味します。その直後の最初の文が SELECT であれば、読み取りトランザクション中です。同じトランザクション内で後続の UPDATE が書き込みトランザクションへの昇格を必要とし、読み取り開始後に別の接続が書き込みを行っていた場合、SQLite は待機させられません。スナップショットがすでに古くなっており、待機すると 2 つの接続が互いにデッドロックするためです。ドキュメントにも結果が直接記載されています。
後続の書き込み文は、可能であればトランザクションを書き込みトランザクションへ昇格させます。昇格できない場合は SQLITE_BUSY を返します。
設定した 5000 ミリ秒のタイムアウトは参照されません。エラーは即座に返されるため、設定が機能していないように見えます。
修正方法は 1 語です。
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE は、読み取りを開始する前に、最初から書き込みロックを取得します。昇格が発生しないため、回避すべきデッドロックもありません。その結果、busy handler が適用され、接続は失敗せずに順番を待ちます。読み取り専用のトランザクションは deferred のままにします。書き込みを含むトランザクションは immediate にしてください。
ロックエラーの 2 つ目の原因は、書き込みトランザクションを時間のかかる処理にまたがって保持することです。SQLite は書き込み処理を直列化するため、トランザクションを開始してネットワーク経由で外部 API を呼び出し、その後に commit する処理では、その呼び出し中ずっと他の書き込み処理がブロックされます。必要なデータを読み取り、トランザクションを閉じてから時間のかかる処理を実行します。その後、結果を保存するための短い書き込みトランザクションを開始してください。
Litestream による継続的なバックアップ
毎晩コピーする方法では、最大 1 日分の書き込みが失われます。また、稼働中の SQLite データベースに対して cp を実行すると、開けないコピーが生成されることがあります。安全な方法は 2 つあります。sqlite3 app.db ".backup /path/to/backup.db" は SQLite のオンラインバックアップインターフェースを使用するため、使用中のデータベースに対して実行できます。Litestream はさらに進んだ仕組みです。WAL を監視し、変更をオブジェクトストレージへ継続的に送信します。これにより、最悪時のデータ損失を 1 日分から約 1 秒分に減らせます。
Litestream は、アプリケーションの隣で実行する 1 つの Go バイナリです。アプリケーションとデータベースの間には入りません。アプリケーションはこれまでどおり 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 version2026 年 7 月時点で、公式の Linux インストールページに記載されているリリースは v0.5.14 です。その後、2026 年 7 月 21 日に v0.5.15 がリリースされました。両方の行のバージョンを releases ページの現在のタグに合わせて変更してください。VPS が arm64 の場合は、対応する arm64 パッケージを使用してください。
設定ファイルは /etc/litestream.yml にあります。まずローカルファイルレプリカから始めてください。クラウドの認証情報なしで、処理全体が正しく動作することを確認できます。
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/appフィールド名は単数形の replica である点に注意してください。Litestream 0.5 では、0.3 系列の replicas 配列が単一のレプリカブロックに置き換えられました。現在、2 つのエントリを含む設定では起動時に失敗します。古い配列を示すサードパーティーのガイドが今も多くあります。検索結果の最初に表示された例ではなく、上記の形式をそのまま使用してください。0.5 系列では、ディスク上のバックアップ形式が変更されたため、litestream wal サブコマンドの名前も litestream ltx に変更されています。
有効化する前に、設定を解析できることを確認してください。
sudo litestream databases -config /etc/litestream.yml次に、手動で一連の処理を確認します。この形式では設定ファイルを使用せず、1 つのデータベースを 1 つのパスにレプリケートします。
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/appこれはフォアグラウンドで実行され、動作し続けます。別のシェルで行を 1 つ書き込み、レプリカを新しいファイルへリストアしてください。
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 秒がリカバリーポイントにもなります。クラッシュが発生すると、最後の同期間隔に行われた書き込みが最大で失われます。どのような設定でも、この損失をゼロにはできません。
実際のストレージを使用する場合は、レプリカブロックを S3 URL に置き換えてください。Amazon S3 と、他のプロバイダーが提供する S3 互換オブジェクトストレージの両方で使用できます。
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24h認証情報をそのファイルに記述しないでください。Litestream は環境変数から LITESTREAM_ACCESS_KEY_ID と LITESTREAM_SECRET_ACCESS_KEY を読み取ります。これらの値は、root が所有し、モード 600 に設定した systemd drop-in に記述してください。
上記のスナップショット関連の値はデフォルト値ですが、保持期間のデフォルト値には注意が必要です。保持期間は、Litestream がスナップショットと、それに関連するファイルを保持する期間です。つまり、どの時点までさかのぼってリストアできるかを決める値でもあります。24 時間では、水曜日の朝に気付いた不正なマイグレーションを、月曜日の状態から復旧できません。retention: 168h を 1 週間に設定し、追加のストレージ料金を支払ってください。
必要になる前にリストアを検証する
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 のサービスとタイマーで定期的に実行し、出力を確認します。バックアップを一度リストアするまで、それが機能するかどうかは分かりません。
systemd で Litestream を実行する
Debian パッケージは、/etc/litestream.yml を読み込む litestream unit をインストールします。
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f正常な出力では、設定に記載された各データベースの名前が表示され、その後は定期的な同期行以外は出力されません。データベースのパスに対する no such file or directory エラーは、設定内のパスが誤っているか、プロセスがそのパスを読み取れないことを示します。unit はデフォルトで root として実行されますが、このジョブには過剰な権限です。Litestream は、データベースと、それを格納するディレクトリの両方を読み書きできる必要があります。データベースと同じ場所にある -wal ファイルと -shm ファイルを扱うためです。そのため、アプリケーションがすでに使用しているアカウントを指定してください。
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appusersudo systemctl daemon-reload と sudo systemctl restart litestream で適用します。最小権限の専用サービスアカウントを設定するには数分かかりますが、これにより、バックアップエージェントをホスト上の 2 つ目の root プロセスにしなくて済みます。
マシンを完全に再構築する場合は、起動順序に注意が必要です。アプリケーションを起動する前に、データベースを復元します。litestream restore は -if-db-not-exists を受け付けます。ファイルがすでに存在する場合は 0 で終了するため、毎回のブートで安全に実行できます。アプリケーションの unit に ExecStartPre 行として指定すると、新しい VPS ではデータベースがダウンロードされ、既存の VPS では何も行われません。1 か所にまとめたい場合は、litestream replicate に対応する -restore-if-db-not-exists フラグがあります。
VPS で SQLite が破綻する条件
ネットワークファイルシステム。 これは設定では回避できない制約です。WAL モードでは、データベースを使用するすべてのプロセスが小さなメモリ領域を共有する必要があります。この領域を提供するのが -shm ファイルです。SQLite のドキュメントには、例外なく次のように記載されています。
データベースを使用するすべてのプロセスは同じホストコンピューター上で実行する必要があります。WAL はネットワークファイルシステム上では動作しません。
そのため、マウントした NFS(ネットワークファイルシステム)や SMB 共有上のデータベースでは破損が発生する可能性があり、これを防ぐ pragma はありません。ここでは、見落とされがちな違いがあります。ネットワーク ブロック デバイスは、多くの VPS プロバイダーが追加ストレージとして接続するもので、Linux からは通常のファイルシステムを備えた通常のディスクとして見えます。この構成には問題ありません。一方、マウントした ファイル共有 は別物です。
2 台目のアプリケーションサーバー。 これを動作させる設定はありません。2 台のマシンで同じデータを提供する必要が生じたら、ネットワーク経由で通信するデータベースが必要です。計画する時間が残っているうちに、その移行を判断してください。
書き込みが多いワークロード。 同時に書き込めるのは一度に 1 つのライターだけです。これはファイル形式の性質であり、調整できる設定ではありません。各コミットが WAL への追記になるため、短い書き込みは低コストです。そのためスループットは CPU よりも、ディスクの小さな書き込みに対するレイテンシーに大きく左右されます。この違いについては、VPS における NVMe と SATA SSD ストレージの比較を参照してください。本当の問題は長いトランザクションです。長いトランザクションが、他のすべてのライターを後ろで待機させるためです。
分析クエリ。 SQLite はトランザクション向けに構築された行指向ストアです。1 億行を走査するダッシュボードは、別のツールに適した別の処理です。適用範囲の境界については、サーバー用途における DuckDB と SQLite の比較で説明しています。
レプリケーション中の VACUUM。 完全な VACUUM ではデータベースファイル全体を書き換えるため、Litestream はファイル全体を再度アップロードする必要があります。また、Litestream のドキュメントでは、レプリケーション中にその場で実行しないよう推奨しています。レプリケーターを停止して vacuum を実行し、再度起動してください。新しい完全スナップショットが作成されます。
1 つのデータベースに対する 2 つのレプリケーター。 同じデータベースまたは同じレプリカの保存先に対して、2 つの Litestream プロセスを実行しないでください。これを防ぐ責任は利用者にあると、ドキュメントに明記されています。実行すると、復元できないレプリカが作成されます。
Litestream が対象としないもの
Litestream が保護するのはデータベースファイルだけです。アップロードされたファイル、アプリケーション設定、TLS(Transport Layer Security)証明書、unit ファイルは、引き続き自分で管理する必要があります。定期的に restic を使用した暗号化済みの別ホストへのバックアップと組み合わせれば、両方をカバーできます。マシンが新しい場合は、新しい VPS で最初の 10 分間に行う作業で、このガイドが完了済みとして扱うユーザーアカウントとファイアウォールの設定を実施できます。
FAQ
本番アプリケーションには SQLite で十分ですか?
1 台のサーバーで 1 つのアプリケーションを運用するなら、WAL モードを有効にし、busy timeout を設定し、継続的にバックアップすることを条件に、十分です。重要な制約は構造上のものです。同時に書き込めるのは 1 つのライターだけで、利用できるホストマシンも 1 台だけです。この制約内に収まるアプリケーションであれば、ネットワークを経由せず、監視対象となる別プロセスもないデータベースを利用できます。収まらないアプリケーションには client-server データベースが必要です。どれだけチューニングしても、この制約は変わりません。
busy_timeout を設定したのに、なぜ database is locked が発生するのですか?
SQLite は、待機するとデッドロックが発生する可能性がある場合、busy handler を呼び出さないためです。単独の BEGIN で開始したトランザクションは deferred です。開始時の SELECT により read transaction になり、後続の書き込みで read transaction からの昇格が必要になります。その間に別の接続が書き込みを行うと、読み取りスナップショットがすでに古くなっているため、SQLite は busy handler を呼び出さず、直ちに SQLITE_BUSY を返します。書き込みを行うトランザクションは、BEGIN IMMEDIATE で開始してください。これにより最初に write lock が取得され、タイムアウトが適用されます。
SQLite データベースをネットワークストレージに置けますか?
NFS や SMB などのネットワークファイルシステムには置かないでください。WAL モードでは、すべてのプロセスが -shm ファイルを通じてメモリを共有する必要があります。また、SQLite のドキュメントでは、データベースを使用するすべてのプロセスが同じホストコンピューター上に存在する必要があると説明されています。プロバイダーが接続するネットワークブロックデバイスは別のものです。Linux からは通常のファイルシステムを備えた通常のディスクとして認識されるため、SQLite はそこで動作します。
nightly backups をすでに実行している場合、Litestream は必要ですか?
どれだけのデータ損失を許容できるかによります。nightly job では、最大 24 時間分の書き込みを失う可能性があります。Litestream は約 1 秒ごとに同期するため、クラッシュ時の損失はおおむね直前の 1 秒分です。また、cp でデータベースファイルをコピーする方法より安全です。この方法では、書き込み途中のデータベースを取得する可能性があります。Litestream が対象とするのはデータベースだけです。そのため、別途、一般的なファイルバックアップも実行してください。