Azure App ServiceでSQLiteを安全にバックアップ・リストアする完全ガイド|Litestream・PM2の要否まで解説

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 方式)

  1. App Service を停止(アプリが DB を掴んでいない状態にする)。
  2. 対象のバックアップを Blob からダウンロード(azcopy copy など)。
  3. /home/site/wwwroot/data/ などの DB 配置ディレクトリで、既存 app.db と app.db-wal, app.db-shm を退避。
  4. 解凍して app.db として配置。パーミッションを chmod 640 など適切に。
  5. 念のため sqlite3 app.db "PRAGMA integrity_check;" を実行。
  6. 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 は原則不要、必要な場合も単一プロセスで。バックアップは「撮る」「送る」「残す」「戻す」を一貫して自動化し、障害時の心理的・時間的コストを限りなくゼロに近づけましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次