Azure App Service(Linux コンテナ)上で Node.js と SQLite を組み合わせると、低コスト・低運用負荷で堅実な Web アプリが構築できます。ただし「どこに DB を置くか」「どうやって確実にバックアップ/リストアするか」を設計しておかないと、いざという時に戻せません。本記事では、オンディスク SQLite を前提に、現場で使えるバックアップ戦略と復旧手順、PM2 の是非、常駐ツールの組み込み方、運用の落とし穴までを一気に解説します。
Azure App Service 上で SQLite を安全に扱うための前提
最初に、App Service(Linux コンテナ)でのファイル配置とライフサイクルを把握しておきます。コンテナのルートファイルシステムは再デプロイやスケール操作で入れ替わる一方、/home(内部的には /home/site/wwwroot など)配下は永続ストレージとしてマウントされ、インスタンスの再起動を跨いで保持されます。ただし、この領域はネットワーク越しの共有ストレージであり、ローカル SSD ほど速くありません。したがって、SQLite の DB ファイルは /home 配下に置き、性能に過度な期待をせず、適切なジャーナル設定とバックアップ設計を行うのが鉄則です。
| 場所 | 永続性 | 用途の目安 | 注意点 |
|---|---|---|---|
/home/site/wwwroot など | 永続(再起動・デプロイを跨いで維持) | アプリ本体、SQLite DB、バックアップ一時退避 | ネットワークストレージで I/O が遅い。高頻度書込みは不利 |
/tmp | 非永続(インスタンス寿命まで) | 一時ファイル | 再起動で消える。DB 置き場には不適 |
| コンテナ・ルート FS | 再デプロイで再作成 | 実行バイナリ、依存ツール | 変更はコンテナ再作成が必要 |
SQLite のバックアップ/リストア方式の全体像
要件(RPO=どこまで巻き戻せるか、RTO=どれくらいで復旧できるか、コスト、運用の容易さ)に応じて、次の 3 パターンを使い分けます。
| 方法 | 概要 | 長所 | 短所・注意点 | 想定 RPO / RTO | コスト感 |
|---|---|---|---|---|---|
sqlite3 .backup → Azure Blob へコピー | cron / WebJob / スクリプトで定期的に DB のスナップショットを作成し、Blob コンテナへ世代保管 | 追加ライブラリ不要、構成が単純、低コスト | 世代管理・削除は自前、実行間隔が粗いと RPO が悪化 | RPO: 実行間隔(例 5〜60分)、RTO: 数分〜十数分 | Blob 容量課金のみ |
| Litestream でストリーミング複製 | WAL をリアルタイムにリモートへレプリケーション。任意の時点へリストア可能 | 秒〜分単位の復旧(低 RPO)、差分転送で効率的 | 常駐プロセスの導入・運用が必要。起動スクリプトの調整が必須 | RPO: 秒〜数十秒、RTO: 数分 | Blob+転送量に応じた少額 |
| Azure Backup(サイト丸ごと) | App Service のスナップショットとして一括取得 | GUI 中心で統合管理、他資産と合わせて一元化可 | SQLite だけなら過剰、料金も相対的に高い | RPO: 1日〜、RTO: テナント・規模に依存 | バックアップ構成に準拠 |
共通の落とし穴と対策
- WAL モードを必ず有効化:共有ストレージでの書込み安全性・同時実行性が向上し、オンラインバックアップの整合性も取りやすくなります。
- 水平スケールは基本 NG:SQLite は単一プロセス/単一ノードでの排他制御を前提にしています。App Service のインスタンスを複数に増やすとファイルロック競合が発生し破損リスクが高まります。常に単一インスタンス運用を基本にしましょう。
- リストア時はサイト停止→置換→起動の順:稼働中に DB ファイルを上書きすると破損します。停止中に置換し、起動時にマイグレーション確認まで含めます。
- バックアップの検証を自動化:取得だけで満足せず、別スロット/別リソースでの定期的なリストア・リハーサルを自動化し、実効性を常に担保します。
方式 1:sqlite3 .backup + Blob へ世代保管
WAL(Write-Ahead Logging)を有効化
初期化時に一度だけ有効化しておきます。Node.js からでも sqlite3 / better-sqlite3 いずれでも構いません。
PRAGMA journal_mode=WAL;
PRAGMA wal_autocheckpoint=1000; -- 目安。書込み頻度に応じて調整
PRAGMA synchronous=NORMAL; -- 性能と安全性のバランス
PRAGMA busy_timeout=5000; -- 競合時の再試行猶予
バックアップ・スクリプトの例
App Service(コンテナ)内で以下のようなスクリプトを用意します。azcopy は単体バイナリで配布されるためコンテナに組み込みやすく、Managed Identity と組み合わせて安全にアップロードできます。
#!/usr/bin/env bash
set -euo pipefail
APP_DB="/home/site/wwwroot/data/app.db"
WORKDIR="/home/site/wwwroot/backup-tmp"
STAMP="$(date +%Y%m%d-%H%M%S)"
BASENAME="app-${STAMP}.db"
OUT="${WORKDIR}/${BASENAME}"
mkdir -p "${WORKDIR}"
# アプリに優しく:直近のトランザクションをチェックポイント
sqlite3 "${APP_DB}" "PRAGMA wal_checkpoint(TRUNCATE);"
# 一貫性のあるスナップショットを生成
sqlite3 "${APP_DB}" ".backup '${OUT}'"
# 圧縮(任意)
gzip -9 "${OUT}"
OUT="${OUT}.gz"
# Managed Identity ログイン(システム割当 ID 前提)
# 事前にストレージの「データ共同作成者」ロールを付与しておく
azcopy login --identity
# Blob へコピー(Retention/世代管理は後述)
# 例)https://.blob.core.windows.net//prefix/
DEST="https://.blob.core.windows.net//sqlite-backups/${BASENAME}.gz"
azcopy copy "${OUT}" "${DEST}" --overwrite=false
# ローカル一時ファイルを整理(世代 N を残したい場合は find -mtime などで調整)
rm -f "${OUT}"
echo "Backup done: ${DEST}"
スケジューリング(Triggered WebJob で CRON 実行)
App Service の WebJobs(Triggered)を使えば、Git デプロイと一緒にスクリプトを配備し、CRON 形式で定期実行できます。
- 配置:
/home/site/wwwroot/App_Data/jobs/triggered/backup-db/にrun.shとsettings.jobを置く。 settings.jobの例:
{
"schedule": "0 */15 * * * *",
"stopping_wait_time": 300
}
上記は 15 分おきに実行する例です。ログは Kudu/診断ログから確認できます。
世代管理とライフサイクル
- 命名規則は
app-YYYYMMDD-HHMMSS.db.gzのように単調増加にしておくと整理が容易。 - Blob 側のライフサイクル管理で「最後のアクセス日時から N 日後に削除」などを設定しておくと、古い世代の削除を自動化できます。
- 週次・月次の長期保存が要る場合はプレフィックスを分ける(例:
daily/,weekly/,monthly/)。
リストア手順(.backup 方式)
- App Service を停止(アプリが DB を掴んでいない状態にする)。
- 対象のバックアップを Blob からダウンロード(
azcopy copyなど)。 /home/site/wwwroot/data/などの DB 配置ディレクトリで、既存app.dbとapp.db-wal,app.db-shmを退避。- 解凍して
app.dbとして配置。パーミッションをchmod 640など適切に。 - 念のため
sqlite3 app.db "PRAGMA integrity_check;"を実行。 - App Service を起動。起動時に DB マイグレーション(例:Knex / Prisma)を走らせ、スキーマ整合を確認。
方式 2:Litestream によるストリーミング複製
より低い RPO を求める場合は Litestream が有力です。ポイントは「アプリと同じコンテナ内で常駐させ、WAL の変更を即時にリモートへ転送」する設計です。
構成パターン
- シンプル構成:Litestream → リモート(S3 互換 / Azure 連携)。
- 二段構え:Litestream → ローカル
/home/backupsに世代保管 → バッチで Blob に集約(帯域や互換性の都合で採用)。
コンテナへの組み込み例(Dockerfile 抜粋)
# ベース: Node.js ランタイム
FROM node:20-bookworm
# アプリとツールを追加
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
# sqlite3 / better-sqlite3 を使う場合のビルド依存は適宜追加
RUN apt-get update && apt-get install -y sqlite3 ca-certificates curl && rm -rf /var/lib/apt/lists/*
# azcopy 単体バイナリ(必要なら)と litestream を取得
# 実際の URL は固定化せずビルド時引数や社内アーティファクトから供給するのが安全
# RUN curl -L -o /usr/local/bin/azcopy && chmod +x /usr/local/bin/azcopy
# RUN curl -L -o /usr/local/bin/litestream && chmod +x /usr/local/bin/litestream
COPY . .
# 起動スクリプト
COPY startup.sh /usr/local/bin/startup.sh
RUN chmod +x /usr/local/bin/startup.sh
CMD ["/usr/local/bin/startup.sh"]
Litestream の設定例
環境変数でクレデンシャルを注入し、設定ファイルには機密を埋め込まない方針が無難です。以下は概念的な例です。
# /etc/litestream.yml
dbs:
- path: /home/site/wwwroot/data/app.db
replicas:
- url: s3://your-bucket/sqlite/app.db
region: us-east-1
endpoint: https://s3-compatible.example # S3 互換エンドポイントを使う場合
# access-key-id / secret-access-key は環境変数で供給
snapshot-interval: 1m
retention: 30d
起動スクリプト(アプリと Litestream を同居)
#!/usr/bin/env bash
set -euo pipefail
DB="/home/site/wwwroot/data/app.db"
DB_DIR="$(dirname "$DB")"
mkdir -p "${DB_DIR}"
# 初回起動時、リモートスナップショットがあればリストア
if [ ! -f "${DB}" ]; then
echo "Restoring DB from replica if exists..."
litestream restore -if-replica-exists "${DB}" || true
fi
# アプリを Litestream が監視する形で起動
exec litestream replicate -exec "node server.js" /etc/litestream.yml
リストア(任意時点に戻す)
# サイトを停止 → DB を退避
# 例)litestream restore -o /path/to/restore.db -timestamp "2025-10-31T10:00:00Z" /etc/litestream.yml
# リストア後に置換し、integrity_check → サイト起動
補足: Litestream は主に S3 互換リモートへの複製を前提とした設計です。Azure Blob を使う場合は、S3 互換のエンドポイント(ゲートウェイ)を経由する、またはローカルへスナップショット保存後に azcopy で Blob に集約する、といった組み合わせが実務上扱いやすいパターンです。環境ポリシーに合わせて採用してください。
方式 3:Azure Backup(サイト全体のスナップショット)
運用資産を一括してバックアップ管理したい場合に検討します。DB だけを安価に保護したい要件では過剰になりやすい一方、復旧手順の標準化という観点ではメリットがあります。RPO はスケジュール次第ですが、アプリコードも込みで丸ごと戻せる点が特徴です。
PM2 は必要か:App Service のプロセス管理
結論:基本は不要(App Service 標準の監視で十分)
- App Service は
package.jsonのstartを監視し、プロセスが落ちれば自動再起動します。 - メモリ上限・再起動ポリシー、ヘルスチェックによる再起動など、ポータル設定だけで多くの要件を満たせます。
- スロット(ステージング/本番)切替により ゼロダウンタイム に近いデプロイが可能で、PM2 のクラスタリングやホットリロードに頼らなくても実務は回せます。
それでも PM2 を使いたい場合の最小構成
App Service 既定のエントリポイントを PM2 に置き換える必要があります。コンテナでは起動スクリプトを用意し、--no-daemon でフォアグラウンド実行にします(デーモン化すると監視されなくなるため)。
#!/usr/bin/env bash
set -euo pipefail
# PM2 を使う場合
pm2 start server.js --name app --no-daemon --time
ただし、PM2 クラスタモード(-i max)でワーカーを増やすと SQLite の単一ファイル I/O に負担がかかり、ロック競合・レイテンシ増大の原因になります。単一プロセスを基本とし、スケールが必要ならデータストア自体の見直し(Azure SQL / Cosmos DB など)を検討しましょう。
追加ツール/スクリプトの配置と運用
| ユースケース | 推奨手段 | 設定ポイント |
|---|---|---|
| 定期バックアップ等のバッチ | WebJobs(Triggered) | ディレクトリに run.sh と settings.job。CRON で実行。ログは診断ログで確認 |
| Litestream など常駐プロセス | カスタム起動スクリプト | startup.sh で litestream replicate -exec "node server.js" を起動。wait でフォアグラウンド維持 |
| ログの分離 | Application Insights + プレフィックス | STDOUT ログの先頭に [APP] / [LITE] / [JOB] を付与し、クエリで抽出しやすくする |
起動コマンドと環境変数
- App Service は
PORTを環境変数で割り当てます。server.listen(process.env.PORT)を忘れずに。 - タイムアウトや起動待ち時間は App 設定(例:
WEBSITES_CONTAINER_START_TIME_LIMIT)で調整可能。 - Managed Identity を使う場合は、ストレージ側に適切な RBAC(例:「ストレージ BLOB データ共同作成者」)を付与します。
復旧戦略を設計する:RPO/RTO と演習
バックアップは「撮る」だけでは不十分です。ビジネスとして許容できる RPO/RTO を数値で定義し、その値を満たす運用(スケジュール、監視、手順)を確立します。
| シナリオ | 想定 RPO | 想定 RTO | 推奨ソリューション |
|---|---|---|---|
| 中小規模・更新頻度が低い | 15〜60 分 | 〜15 分 | .backup + Blob、日次で整合性検証 |
| 更新頻度が中〜高い | 数十秒〜数分 | 〜10 分 | Litestream で継続レプリカ + 任意時点復旧 |
| アプリ全体の一括復旧が必要 | 1 日 | 環境に依存 | Azure Backup を補助的に併用 |
実践チェックリスト(バックアップ/リストア)
バックアップ時
- DB は
/home配下に配置し、権限は最小限(所有者のみ読み書き)。 - WAL を有効化し、バックアップ直前に
wal_checkpointを実施。 - バックアップ後に
sqlite3 <dump> "PRAGMA integrity_check;"を実行し、ログに記録。 - 転送は
azcopy(Managed Identity)で安全に。SAS を使う場合は短寿命にし、再利用しない。 - Blob 側の階層は
/yyyy/mm/dd/などに整理し、ライフサイクルで自動削除。
リストア時
- アプリ停止 → 既存 DB のフルバックアップ(念のため)→ 対象世代を取得 → 置換 → 整合性チェック → 起動。
- スキーママイグレーションのダウングレードが必要な場合に備え、アプリの該当バージョンに切り替え可能なようスロットやタグを運用。
- 復旧演習は四半期ごとに最低 1 回、手順書の更新を徹底。
性能チューニングの基本(SQLite × 共有ストレージ)
- PRAGMA:
journal_mode=WAL、synchronous=NORMAL、busy_timeout設定。 - 接続ライブラリ:高頻度 I/O なら
better-sqlite3(同期 API)でコネクションを 1 本に束ねる構成が扱いやすい。 - ライトパスの削減:バルク書込みをトランザクションでまとめる。無駄な
VACUUMを避け、メンテはメンテナンスタイムに。 - インデックス:読み取りパスが多いキーに適切なインデックスを付与。EXPLAIN QUERY PLAN を確認。
- ログ分離:I/O 大のバッチは WebJobs に切り出し、オンライン処理を軽く保つ。
セキュリティとコンプライアンス
- Managed Identity:Blob への書込みは ID ベースで許可。接続文字列やキーの平文保管を避ける。
- 暗号化:Blob はサーバー側暗号化(SSE)が既定。アプリ側で追加暗号化が必要な場合は暗号化アーカイブ(AES)化を検討。
- 監査ログ:バックアップ成功/失敗を Application Insights のカスタムイベントに送信し、アラートを設定。
- リージョン冗長:既定の LRS で十分なケースが多いが、地理冗長が要件なら GRS/RA-GRS を選択。
CI/CD と自動化:置き忘れをゼロにする
- コンテナビルド:Dockerfile に
sqlite3,azcopy,litestreamの導入を明記。 - デプロイ:GitHub Actions 等でイメージをビルド→コンテナレジストリへ push→App Service へ更新。
- 構成の一体化:同じパイプラインで WebJobs ディレクトリや
startup.sh、litestream.ymlも配布。 - 運用検証:デプロイ完了後にヘルスチェック(アプリの
/healthz)と バックアップジョブの試験実行を自動化。
実用サンプル:最小構成の Node.js
// server.js
const http = require('http');
const Database = require('better-sqlite3');
const dbPath = process.env.DB_PATH || '/home/site/wwwroot/data/app.db';
const db = new Database(dbPath);
// 初回起動時にテーブル作成
db.pragma('journal_mode = WAL');
db.exec(` CREATE TABLE IF NOT EXISTS messages(
id INTEGER PRIMARY KEY AUTOINCREMENT,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);`);
const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
try { db.prepare('SELECT 1').get(); res.writeHead(200); res.end('ok'); }
catch (e) { res.writeHead(500); res.end('db-error'); }
return;
}
if (req.method === 'POST' && req.url === '/messages') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
db.prepare('INSERT INTO messages(body) VALUES(?)').run(body || 'hello');
res.writeHead(201); res.end('created');
});
return;
}
if (req.method === 'GET' && req.url === '/messages') {
const rows = db.prepare('SELECT id, body, created_at FROM messages ORDER BY id DESC LIMIT 50').all();
res.writeHead(200, {'Content-Type':'application/json'}); res.end(JSON.stringify(rows));
return;
}
res.writeHead(404); res.end('not found');
});
const port = process.env.PORT || 3000;
server.listen(port, () => console.log(`[APP] listening on ${port}`));
トラブルシュート:よくある症状と対処
| 症状 | 原因の可能性 | 対処 |
|---|---|---|
| バックアップファイルが壊れている | 稼働中にファイルコピーした、WAL 未チェックポイント | sqlite3 .backup を使う/直前に wal_checkpoint 実行/アプリ停止中にリストア |
| 書込みが詰まる・タイムアウト | 共有ストレージ I/O、同時アクセス過多 | トランザクションでまとめる、busy_timeout を設定、バッチを WebJobs に分離 |
| 横にスケールするとエラーが出る | 複数インスタンスでファイルロック競合 | シングルインスタンス固定。スケール必要ならデータストアを見直す |
| 復旧後にアプリが起動しない | DB スキーマとアプリのバージョン不一致 | 復旧対象世代に合わせてアプリのリリースも切り替える(デプロイスロット活用) |
まとめ:最小コストで最大の安心を
App Service × SQLite でも、.backup+Blob または Litestream を適材適所に選べば、現実的な RPO/RTO を満たす運用が成立します。重要なのは、WAL 前提の設計、単一インスタンス運用、停止してからリストア、そして演習の自動化です。PM2 は原則不要、必要な場合も単一プロセスで。バックアップは「撮る」「送る」「残す」「戻す」を一貫して自動化し、障害時の心理的・時間的コストを限りなくゼロに近づけましょう。

コメント