SSD Nodes Learn メモリ 8GB — 年額 $66
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-01

VPSの本番環境でSQLiteを使う設定と限界

VPSの小規模アプリならSQLiteが本番データベースとして適しています。WAL、busy_timeout、Litestreamによる複製と、移行が必要になる同時書き込みや複数台構成の限界を解説します。

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

SQLiteがVPS上の本番データベースとして適している場合

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.dbapp.db-walapp.db-shm です。-wal ファイルには、まだチェックポイント処理されていないコミット済みページが格納されます。-shm ファイルは共有メモリインデックスです。すべての接続がこのファイルをマッピングし、WALの内容について同じ状態を認識します。どちらもデータベースの一部であり、一時ファイルではありません。アプリケーションの実行中に app.db だけをコピーすると、直近のコミットがすべて欠落したファイルになります。app.db を削除して他の2つを残すと、SQLiteは古いWALページを、その名前で新しく作成されたファイルに適用します。このため、データベースをリセットしようとして新しいデータベースを破損することがあります。

本番アプリケーションに必要な接続設定

データベースに保存されるのは journal_mode だけです。以下のその他の設定はすべて接続ごとに適用されます。そのため、アプリケーションは開く接続ごとに、バックグラウンドで connection pool が作成する接続も含めて、これらを実行する必要があります。

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;

busy_timeout = 5000 は、database is locked を返す前に、ロックされたデータベースに対して最大 5000 milliseconds 再試行するよう SQLite に指示します。デフォルトは 0 です。そのためデフォルトでは、2つの writer が初めて重なった時点で SQLite はすぐに失敗します。この値を1つ設定するだけで、SQLite 自体が原因だと誤解される lock エラーの大半を解消できます。

synchronous = NORMAL は WAL mode に適した設定です。ただし、そのトレードオフを理解しておく必要があります。FULL では、SQLite は commit ごとに WAL に対して fsync を呼び出します。NORMAL では、checkpoint の時点で同期します。SQLite のドキュメントは、失われるものを明確に説明しています。電源障害または hard reset の後、transaction は durable ではなくなります。電源喪失によってデータベースが破損することはありませんが、ディスクに到達していなかった最後の commit は失われます。VPS では、通常このトレードオフが適切です。すべての write の経路から fsync を1回ずつ取り除けるためです。

foreign_keys = ON は backwards compatibility のためデフォルトで無効で、接続ごとに設定されます。REFERENCES clause を含む schema でも、各接続でこれを有効にするまでは何も強制されません。

後で重要になる設定がもう1つあります。SQLite は WAL が 1000 pages を超えると自動的に checkpoint を実行します。この処理は、その時点で transaction を完了した接続が担当します。これだけなら問題ありません。ただし Litestream を実行している場合は、checkpoint の実行タイミングを Litestream が制御したいため、考慮が必要になります。

busy_timeoutを設定した後もdatabase is lockedが発生する理由

これはPostgresに戻る原因となるエラーで、原因は1つです。

busy timeoutはbusy handlerを登録します。ただし、SQLiteはbusy handlerを呼び出すことを保証していません。

SQLiteがbusy handlerの呼び出しによってデッドロックが発生する可能性があると判断した場合、busy handlerを呼び出さず、代わりにアプリケーションへSQLITE_BUSYを返します。

回避されるデッドロックは、トランザクションの昇格時に発生します。SQLiteで単独のBEGINBEGIN DEFERREDを意味します。その直後の最初の文がSELECTの場合、読み取りトランザクションが開始されています。同じトランザクション内で、後続のUPDATEが書き込みトランザクションへの昇格を必要とし、読み取り開始後に別の接続が書き込みを行っていた場合、SQLiteは待機させることができません。スナップショットがすでに古く、待機すると2つの接続が互いにデッドロックするためです。ドキュメントには結果が直接示されています。

後続の書き込み文は、可能であればトランザクションを書き込みトランザクションへ昇格させます。昇格できない場合はSQLITE_BUSYを返します。

5000ミリ秒のtimeoutは参照されません。エラーは直ちに返されるため、設定が無視されたように見えます。

修正方法は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 version

v0.5.14は、2026年7月時点で公式のLinuxインストールページに記載されているリリースです。v0.5.15は2026年7月21日に続いてリリースされました。リリースページの現在のタグに合わせて、両方の行のバージョンを変更してください。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

このコマンドはフォアグラウンドで実行され、継続して動作します。別のシェルで行を追加し、レプリカを新しいファイルにリストアしてください。

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_IDLITESTREAM_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_checkok を出力します。それ以外の出力の場合、リストアしたコピーは使用できません。systemd service と timer を使って定期的に実行し、出力を確認してください。バックアップを一度リストアするまで、正常に機能するかどうかは分かりません。

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=appuser

sudo systemctl daemon-reloadsudo 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(network file system)や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台だけです。この制限内に収まるアプリケーションでは、ネットワークを経由せず、監視する別プロセスも必要ないデータベースを使用できます。制限を超えるアプリケーションにはクライアント・サーバー型データベースが必要です。チューニングを重ねても、この制限は変わりません。

busy_timeoutを設定しても、なぜdatabase is lockedが発生するのですか?

待機によってデッドロックが発生する可能性がある場合、SQLiteはbusy handlerを呼び出さないためです。単独のBEGINで開始するトランザクションは遅延トランザクションです。最初のSELECTで読み取りトランザクションになり、後から書き込みへ移行する必要があります。その間に別の接続が書き込みを行うと、SQLiteはbusy handlerを呼び出さず、すぐにSQLITE_BUSYを返します。読み取りスナップショットがすでに古くなっているためです。書き込みを行うトランザクションはBEGIN IMMEDIATEで開始してください。これにより、最初に書き込みロックを取得し、タイムアウトが適用されます。

SQLiteデータベースをネットワークストレージに置けますか?

NFSやSMBなどのネットワークファイルシステムには置けません。WALモードでは、すべてのプロセスが-shmファイルを通じてメモリを共有する必要があります。また、SQLiteのドキュメントでは、データベースを使用するすべてのプロセスが同じホストコンピューター上に存在する必要があると説明されています。プロバイダーが接続するネットワークブロックデバイスは別のものです。Linuxからは通常のファイルシステム上の通常のディスクとして認識されるため、SQLiteはそこで動作します。

夜間バックアップをすでに実行している場合、Litestreamは必要ですか?

どの程度のデータ損失を許容できるかによります。夜間ジョブでは、最大24時間分の書き込みを失う可能性があります。Litestreamは約1秒ごとに同期するため、クラッシュ時の損失はおおむね直前の1秒分です。また、データベースファイルをcpでコピーする方法より安全です。この方法では、書き込み途中のデータベースを取得する可能性があります。Litestreamが対象にするのはデータベースだけです。そのため、一般的なファイルバックアップも並行して実行してください。

#sqlite#wal#litestream#backups#production