VPSでSQLiteを本番運用する方法と限界
1台のVPSに小規模アプリを構築するならSQLiteは有力です。WAL、busy_timeout、Litestreamによるバックアップ設定と、同時書き込み1つなどの限界を解説します。
VPS で SQLite が本番データベースに適している場合
小規模なアプリケーションの多くでは、VPS 上で SQLite を本番運用するのが適切です。理由は単純です。1 台のマシン上で 1 つのプロセスが 1 つのファイルに書き込むだけなら、データベースサーバーは必要ありません。監視するデーモンも、ファイアウォールで開放するポートも、定期的に更新するパスワードも、稼働を維持する 2 台目のマシンも不要です。クエリはネットワーク往復ではなく関数呼び出しになるため、40 個のクエリを実行するページのコストは 40 回の関数呼び出しです。
制約は限定的ですが、明確に存在します。SQLite はデータベースファイル全体で同時に 1 つのライターしか許可せず、ファイルを 2 台のマシン間で共有することもできません。1 台の VPS で 1 つのアプリケーションを実行する構成なら、どちらの制約も問題ありません。その構成を超えた瞬間に、どちらも致命的な制約になります。このガイドでは、サーバー上で SQLite を安全に運用するための設定、Litestream による継続的なバックアップ、そして運用を切り替えるべき時点を説明します。
まず、コマンドラインツールをインストールします。以下の操作はすべて 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 はロールバックジャーナルを使用します。ページを変更する前に、元のページを -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 年 10 月時点で、公式の Linux インストールページが記載しているリリースはまだ v0.5.14 です。その後も新しいリリースが公開されており、2026 年 7 月 21 日には v0.5.15、2026 年 8 月 31 日には最新タグの v0.5.17 がリリースされています。リリースページの現在のタグに合わせて、両方の行のバージョンを変更してください。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 秒が復旧時点の間隔でもあります。クラッシュが発生すると、直前の同期間隔に行われた書き込みが最大で失われます。どのような設定でも、これを 0 にはできません。
実際のストレージを使用する場合は、レプリカブロックを 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 時間の場合、水曜日の朝に気付いた不正なマイグレーションを、月曜日の状態から復旧することはすでにできません。Litestream 0.5 では、保持期間はレプリカ単位ではなく、ルートの snapshot ブロックに設定するグローバル値です。1 週間保持する場合は、そのブロックを retention: 168h に変更し、追加のストレージ費用を受け入れてください。レプリカブロック内の retention: 行は 0.3 系列の構文です。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 のサービスとタイマーで定期的に実行し、出力を確認します。バックアップを一度リストアするまで、それが機能するかどうかは分かりません。
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 ファイルを扱うためです。そのため、アプリケーションがすでに実行されているユーザーアカウントを指定します。この例では appuser です。
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appusersudo systemctl daemon-reload と sudo systemctl restart litestream で適用します。最小権限の専用サービスアカウントを作成する設定は数分で完了します。これにより、バックアップエージェントをマシン上の 2 つ目の root プロセスにせずに済みます。
マシンを完全に再構築する場合は、起動順序にも注意が必要です。アプリケーションを起動する前に、データベースを復元します。-if-db-not-exists を受け付ける litestream restore は、ファイルがすでに存在するときに終了コード 0 を返すため、毎回の起動時に安全に実行できます。アプリケーションの unit に ExecStartPre 行として追加すると、新しい 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 INTO です。これは圧縮済みのコピーを別のファイルに書き込み、元のファイルをそのまま残します。インプレースで vacuum を実行する必要がある場合、公式の手順は再起動よりも長くなります。レプリケーターを停止し、vacuum を実行し、データベースの隣にある Litestream の隠し追跡ディレクトリと既存のリモートレプリカデータを削除します。その後、再び起動して新しい完全スナップショットから開始させます。最後の手順により、それまで保持していた復元履歴は失われます。
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 が対象とするのはデータベースだけです。そのため、別途、一般的なファイルバックアップも実行してください。