n8nのSQLiteをcpでバックアップしてはいけない理由と安全な取り方

n8n を自分のサーバーで動かしていると、ワークフローも認証情報も実行の記録も、既定では SQLite のデータベース(~/.n8n/database.sqlite)に入っています。アップグレードの前にこのファイルを cp でコピーしておけば戻せる、と考えがちです。弊社はそのやり方で控えを取り、戻そうとしたときに n8n が起動しなくなりました。この記事では、cp の控えが危ない理由を n8n 本体のソースと SQLite の公式ドキュメントで確かめ、安全な取り方と戻し方をまとめます。
この記事の結論
- n8n 2.19.2 は、SQLite を WAL モードで開きます。WAL モードは、書き込みをまず別のファイル(
database.sqlite-wal)に足していく方式です。最新の中身がdatabase.sqliteだけに入っているとは限りません。 - 動いている n8n のデータベースを
cpで写すと、WAL のファイルとの組がずれたり、書き込みの途中を写したりすることがあります。弊社では、cpで取った控えがPRAGMA integrity_checkでokだったのに、戻すときにSQLITE_CORRUPTが出ました。 - 控えは SQLite 自身に書き出させます。動かしたままなら
sqlite3の.backupかVACUUM INTOを使い、書き出し先は絶対パスで書きます。cpを使うのは、n8n を止めて WAL のファイルが消えたのを確かめてからです。 - 戻すときは n8n を止め、古い
-walと-shmを残さずに置き換えます。認証情報の暗号鍵が入った~/.n8n/configも一緒に控えます。
実際に起きたこと
弊社で動かしているワークフローは、自分で立てた n8n(セルフホスト)の上で動いています。データベースは SQLite です。
アップグレードの前に cp で控えを取った
2026-05-06、n8n を 2.4.6 から 2.19.2 へ上げました。上げる前に、データベースのファイルを cp で写して控えにしました。控えに PRAGMA integrity_check(SQLite のファイルに壊れた所がないかを調べる命令)をかけると、結果は ok でした。
戻そうとしたら n8n が起動しなくなった
上げたあと、2.4.6 に戻そうとしました。控えを 2.4.6 に読ませると、次のエラーが続けて出て、n8n が起動しませんでした。
SQLITE_CORRUPT: database disk image is malformed
控えが使えないので、上げたあとのデータベースだけが正しく読める状態になりました。前の版に戻す道が無くなり、そのまま 2.19.2 で進めました。
弊社の記録には、控えのどこが、なぜ壊れていたのかまでは残っていません。分かっているのは、cp で写した控えは、integrity_check が ok でも戻すときに読めなかったことです。以後、弊社ではデータベースの控えを必ず sqlite3 の .backup で取っています。
下の「原因」に挙げるのは、cp の控えが壊れる仕組みとして公式ドキュメントにあるものです。弊社の件がどれに当たるかは、確かめられていません。
停電の予定に備えた段取りと、そのコマンドの落とし穴
同じ月の 2026-05-29、13:00–15:00 に、n8n を動かしていたマシンの設置場所で点検による停電が予定されました。このときは、12:50 ごろに sqlite3 の .backup で控えを取ってから、マシンを正しく止める段取りにしました。結果として停電はこのマシンの電源には及ばず、止める手順は使わずに済みました。
この記事を書くために段取りのメモを見直すと、控えの書き出し先を二重引用符の中で ~ から書いていました(".backup ~/n8n-backup-20260529.sqlite")。この形では、家のフォルダに控えはできません。理由は直し方の手順1で説明します。
原因
ここからは、n8n 2.19.2 の本体のソースと、SQLite の公式ドキュメントで確かめた内容です。
n8n 2.19.2 は SQLite をいつも WAL モードで開く
SQLite への接続の設定は、@n8n/db の db-connection-options.js にあります。
getSqliteConnectionOptions() {
const { sqlite: sqliteConfig } = this.config;
const { n8nFolder } = this.instanceSettingsConfig;
return {
type: 'sqlite-pooled',
poolSize: sqliteConfig.poolSize,
enableWAL: true,
enableWAL は、設定によらず true です。接続を開くときは、データベースとのやりとりを受け持つ部品(@n8n/typeorm の SqliteLibrary.js)が次を流します。
if (this.options.enableWAL) {
await run(`PRAGMA journal_mode = WAL`);
}
ファイルは n8n のフォルダの database.sqlite です。n8n のフォルダは、環境変数 N8N_USER_FOLDER があればその下の、無ければホームの下の .n8n です(@n8n/config の utils.js)。
公式ドキュメントの環境変数の表には、DB_SQLITE_POOL_SIZE の既定は 0 で、0 なら WAL ではない方式(rollback journal mode)を使う、とあります。しかし 2.19.2 のソースでは既定が 3 で、値は1以上と決められており、WAL は常に有効です。
const sqlitePoolSizeSchema = zod_1.z.coerce.number().int().gte(1);
...
this.database = 'database.sqlite';
this.poolSize = 3;
WAL では、確定した書き込みがまず -wal のファイルに入る
SQLite の公式ドキュメントによると、WAL モードでは元の中身をデータベースのファイルに残したまま、変更を別の WAL のファイルの後ろへ足していきます。書き込みの確定(COMMIT)も、WAL のファイルに印を足すことで起きます。WAL の中身をデータベースのファイルへ書き戻すことを、チェックポイントと呼びます。
つまり n8n が動いている間は、確定したばかりの中身が database.sqlite-wal にしか無いことがあります。同じフォルダには database.sqlite-shm もできます。これは、WAL の中から読みたいページを早く探すための索引(wal-index)を、プロセスどうしで分け合うためのファイルです。
公式ドキュメントは、WAL のファイルはデータベースの状態の一部なので、コピーや移動のときはデータベースと一緒に扱うように、と書いています。離ればなれになると、確定した書き込みが失われるか、ファイルが壊れることがあります。
書き込みの途中を写すと、古い中身と新しい中身が混ざる
SQLite の公式ドキュメントの、データベースが壊れる原因をまとめたページによると、裏で動く自動のバックアップは書き込みの途中でファイルを写すことがあり、その写しは古い中身と新しい中身が混ざって壊れることがあります。同じページには、書き込みが進んでいない間なら写しても安全、ともあります。
ただし WAL モードでは、書き込みが無い瞬間でも、確定した中身が -wal にしか無いことがあります。そのときは database.sqlite だけを写しても足りず、-wal も一緒に写す必要があります。前の節の、WAL のファイルはデータベースと一緒に扱う、という説明のとおりです。
n8n は、スケジュールや Webhook でいつ動き出すか分かりません。動かしたまま書き込みの無い瞬間を選び、-wal と組で cp するのは、現実的ではありません。
戻すときに古い -wal が残っていると壊れやすい
同じページは、壊れることにつながりやすい操作として、次のようなものを挙げています。
- データベースのファイルだけを写し、ジャーナル(書き込みの途中の記録のファイル)を写さない。
- データベースのファイルを別のもので上書きし、元のデータベースのジャーナルを消さない。
控えの database.sqlite を置き直しても、そばに別の時点の -wal が残っていれば、2つの組が合いません。
integrity_check の ok は「戻せる」の保証ではない
PRAGMA integrity_check は、公式ドキュメントの言葉では、データベースの低い層の形式と一貫性を調べる命令です。問題が無ければ ok を1行返します。
調べるのは、渡したファイルの形です。最新の書き込みが入っているかや、戻した先に別の -wal が残っていないかは調べません。弊社の控えも ok で、それでも戻すときに SQLITE_CORRUPT が出ました。
直し方
1. 動かしたまま取るなら sqlite3 の .backup
SQLite のコマンドラインの道具 sqlite3 の .backup を使います。SQLite 自身がデータベースを読んで、別のファイルへ書き出します。
# 控えを取る(書き出し先は絶対パスで書く)
sqlite3 ~/.n8n/database.sqlite ".backup '/path/to/backup/n8n-20261009.sqlite'"
# 控えのファイルを検査する(ok が1行返れば合格)
sqlite3 /path/to/backup/n8n-20261009.sqlite "PRAGMA integrity_check;"
sqlite3 のソース(shell.c.in)を見ると、.backup は SQLite のバックアップ用の関数で写しています。
pBackup = sqlite3_backup_init(pDest, "main", p->db, zDb);
...
while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){}
sqlite3_backup_finish(pBackup);
if( rc==SQLITE_DONE ){
rc = 0;
}else{
shellDatabaseError(pDest);
rc = 1;
}
公式ドキュメントによると、この関数で写し終えた控えは、元のデータベースの一貫した最新の写しになります。WAL の側にある中身も控えに入ります。弊社は、n8n を止めて cp するより短く済むので、動かしたまま .backup で取っています。
書き出し先に ~ を使わない。上のコマンドの1つ目の ~/.n8n/database.sqlite は引用符の外なので、シェルが家のフォルダに置き換えます。2つ目の引用符の中は、文字のまま sqlite3 に渡ります。bash と zsh の説明では、~ が置き換わるのは、語が引用符の外の ~ で始まるときです。
そして sqlite3 は、.backup の書き出し先を、受け取った文字のまま開きます。
rc = sqlite3_open_v2(zDestFile, &pDest,
SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, zVfs);
if( rc!=SQLITE_OK ){
cli_printf(stderr,"Error: cannot open \"%s\"\n", zDestFile);
ファイル名の頭の ~/ を家のフォルダに置き換える処理は、起動するときに渡すデータベースの名前を開く所にはありますが、書き出し先はその処理を通りません。開けなければ Error: cannot open で始まるエラーで止まり、控えはできません。書き出し先は、上のように絶対パスで書きます。
2. .backup が終わらないときは VACUUM INTO
上のコードのとおり、.backup は一度に全部を写すのではなく、100 ページずつ写します。公式ドキュメントによると、写している途中で別のプロセスがデータベースを書き換えると、写し直しが最初から始まります。書き換えが頻繁だと、いつまでも終わらないことがある、とも書かれています。n8n は sqlite3 とは別のプロセスなので、実行が多い時間帯はこれに当たりやすくなります。
そのときは VACUUM INTO を使います。公式ドキュメントは、動いているデータベースの控えを取る方法として、バックアップ用の関数の代わりにこれを挙げています。出来上がるのは、元のデータベースの一貫した写しです。
# 書き出し先のファイルは、まだ無いか空でなければならない
sqlite3 ~/.n8n/database.sqlite "VACUUM INTO '/path/to/backup/n8n-20261009.sqlite'"
消した行の中身は写らないので、控えは元より小さくなることがあります。ファイルが大きすぎて控えに時間がかかるなら、実行の記録がたまっていないかを先に見ます(「n8nの実行履歴が数日で消える原因と自動削除の設定」)。
3. cp を使うなら、n8n を止めて -wal が消えたのを確かめてから
n8n は SIGTERM(普通の停止の合図)を受けると、動いている処理を片付け、最後にデータベースの接続を閉じます。commands/start.js の stopProcess の最後に呼ばれる exitSuccessFully です。
async exitSuccessFully() {
try {
await Promise.all([
CrashJournal.cleanup(),
this.dbConnection.close(),
公式ドキュメントによると、最後の接続が閉じるとき、SQLite は最後のチェックポイントをして、WAL のファイルと -shm を消します。正しく止めれば、中身は database.sqlite の1つにまとまります。
# n8n を止めたあと、-wal と -shm が無いことを確かめる
ls -la ~/.n8n/database.sqlite*
# database.sqlite だけなら写してよい
cp ~/.n8n/database.sqlite /path/to/backup/n8n-20261009.sqlite
気をつける点が2つあります。
- 止まるまでの待ち時間には上限があります。既定は30秒(
N8N_GRACEFUL_SHUTDOWN_TIMEOUT)です。超えると n8n はエラーとして終わり、接続を閉じる処理を通らないので、-walが残ることがあります。 - 見張り役が n8n をすぐ起動し直すことがあります。弊社が以前 n8n を動かしていたマシンでは、プロセスを止めると launchd(macOS の常駐の仕組み)が15秒以内に立て直していました。止めるときはサービスとして止めます(systemd なら
systemctl stop)。
待ち時間の既定は、@n8n/config の generic.config.js にあります。commands/base-command.js はこの値を gracefulShutdownTimeoutInS(秒)として読みます。
this.gracefulShutdownTimeout = 30;
待ち時間を超えたときの処理は、commands/base-command.js にあります。
const errorMsg = `Shutdown timed out after ${this.gracefulShutdownTimeoutInS} seconds`;
await this.exitWithCrash(errorMsg, new Error(errorMsg));
-wal が残っているときは、database.sqlite だけを写さず、手順1の .backup を使います。
4. 暗号鍵の入った config も控える
データベースの中の認証情報は、暗号化されて入っています。n8n は鍵を、n8n のフォルダの config というファイルか、環境変数 N8N_ENCRYPTION_KEY から読みます。どちらも無いと、起動のときに新しい鍵を作って config に書き込みます(n8n-core の instance-settings.js)。
this.settingsFile = path_1.default.join(this.n8nFolder, 'config');
...
const encryptionKey = encryptionKeyFromEnv ?? (0, crypto_1.randomBytes)(24).toString('base64');
データベースの控えだけを新しいマシンに戻すと、n8n は認証情報を暗号化したときとは別の鍵で起動します。config は鍵そのものなので、人の目に触れない場所に控えます。
5. 戻すときは n8n を止め、-wal と -shm を残さない
まず n8n を止め、いまの database.sqlite と、あれば -wal と -shm を、3つとも別の場所へ移します。消さずに移せば、結果が悪かったときに元へ戻せます。
# 1) n8n をサービスとして止める(サービス名は環境に合わせる)
sudo systemctl stop n8n
# 2) いまのファイルを3つとも退避する(無いファイルのエラーは気にしなくてよい)
mkdir -p ~/n8n-before-restore
mv ~/.n8n/database.sqlite ~/.n8n/database.sqlite-wal ~/.n8n/database.sqlite-shm ~/n8n-before-restore/
# 3) 控えを置き、-wal と -shm が無いことを確かめる
cp /path/to/backup/n8n-20261009.sqlite ~/.n8n/database.sqlite
ls -la ~/.n8n/database.sqlite*
# 4) n8n を起動する
sudo systemctl start n8n
戻す先の n8n は、控えを取ったときと同じ版にします。n8n は起動するとき、データベースの形を自分の版に合わせる処理(マイグレーション)を流すからです(commands/base-command.js の init)。
await this.dbConnection
.migrate()
.catch(async (error) => await this.exitWithCrash('There was an error running database migrations', error));
アップグレードをやめて前の版に戻すなら、先に前の版の n8n を入れ直し、上げる前に取った控えを置きます。上げたあとのデータベースは、新しい版の形に書き換わっているからです。
6. 控えは別のマシンにも置く
同じディスクの控えは、マシンごと壊れると一緒に失います。弊社はいま、本番の n8n のデータベースなど、そのサーバーにしか無いものの控えを毎朝 3:40 に取り、3:50 に別のマシンが取りに来る形にしています。
7. Docker で動かしているとき
この節は、弊社の本番で試したものではありません。n8n と Docker の公式ドキュメントで確かめた範囲です。
- n8n の公式の Docker の手順では、
n8n_dataというボリュームを/home/node/.n8nにつなぎ、コンテナを起動し直してもデータが残るようにしています。データベースと鍵のconfigは n8n のフォルダに置かれるので(原因と手順4)、この手順どおりなら、このボリュームの中にあります。 docker stopは、コンテナの中の主なプロセスに SIGTERM を送り、待ち時間を過ぎると SIGKILL(強制終了の合図)で止めます。待ち時間は、コンテナに既定を決めていなければ、Linux のコンテナで10秒です。n8n が止まるまで待つ時間の既定(30秒)より短いので、処理が残っていると、接続を閉じる前に打ち切られることがあります。- 待ち時間は
docker stopの-t(--timeout)で変えられます。止めたあとは、手順3と同じく、ボリュームの中に-walと-shmが残っていないかを確かめてから写します。
直ったかを確かめる
控えが本当に使えるかは、戻してみるまで分かりません。本番に戻す前に、控えの写しで次を確かめます。
- 控えに
PRAGMA integrity_check;をかけ、okが返るかを見ます。これは最低限の確認です。 - 本番と控えで、ワークフローの数と、最後に流れたマイグレーションの名前をくらべます。名前が同じなら、同じ版の形です。
- 控えの写しを、本番と同じ版の n8n に読ませます。
# ワークフローの数(本番と控えでくらべる)
sqlite3 -readonly ~/.n8n/database.sqlite "SELECT COUNT(*) FROM workflow_entity;"
sqlite3 -readonly /path/to/backup/n8n-20261009.sqlite "SELECT COUNT(*) FROM workflow_entity;"
# 最後に流れたマイグレーションの名前
sqlite3 -readonly /path/to/backup/n8n-20261009.sqlite \
"SELECT name FROM migrations ORDER BY timestamp DESC LIMIT 1;"
3つ目は、控えそのものではなく写しを使います。n8n のコマンドは、開いたデータベースにマイグレーションをかけるからです。N8N_USER_FOLDER で本番とは別のフォルダを指し、ワークフローの一覧を出すだけのコマンド list:workflow を流します。
# 控えの写しを、試験用のフォルダの .n8n に置く
mkdir -p /tmp/n8n-restore-test/.n8n
cp /path/to/backup/n8n-20261009.sqlite /tmp/n8n-restore-test/.n8n/database.sqlite
# 試験用のフォルダを指して、ワークフローの ID を1行に1つ出す
N8N_USER_FOLDER=/tmp/n8n-restore-test n8n list:workflow --onlyId
試験用のフォルダには config が無いので、鍵を作ったというログが出ますが、一覧を出すだけなら差し支えありません。エラーが出ずに ID が並び、数が本番と合えば、その控えは今の版の n8n で読めます。
本番に戻したあとは、起動のログに Activated workflow の行が出ているかを見ます。n8n 2.19.2 の active-workflow-manager.js では、有効にしたワークフローのうち、Webhook か、トリガーやポーリング(決まった間隔で見に行く仕組み)を登録したものについて、この行を出します。
if (added.webhooks || added.triggersAndPollers) {
this.logger.info(`Activated workflow ${(0, workflow_formatter_1.formatWorkflow)(dbWorkflow)}`, {
それでも直らないとき
戻したら SQLITE_CORRUPT が出る
まず、~/.n8n/ に古い -wal と -shm が残っていないかを見ます。残っていたら n8n を止め、手順5のとおり退避してから控えを置き直します。それでも出るなら、控えが取った段階で壊れていた可能性があるので、1つ前の控えを試します。
どの控えも使えないときの最後の手が、sqlite3 の .recover です。壊れたファイルから読める部分をできるだけ取り出し、SQL の文に直します。
sqlite3 /path/to/broken.sqlite ".recover" | sqlite3 /path/to/recovered.sqlite
取り出せた中身で n8n がそのまま動くとは限りません。本番に置く前に、「直ったかを確かめる」と同じやり方で写しの上で確かめます。
.backup がエラーで止まる
書き出し先が開けないときは、Error: cannot open で始まるエラーになります。書き出し先が引用符の中の ~ から始まっていないか、そのフォルダがあるかを見ます。
写している途中で止まるときは、手順1のコードのとおり、.backup が戻り値 SQLITE_OK(続きがある)の間だけ続け、SQLITE_DONE(写し終わり)以外で抜けるとエラーにしています。公式ドキュメントでは、ロックが取れずに SQLITE_BUSY が返ったときはやり直せるとされています。時間を置いてやり直すか、手順2の VACUUM INTO を使います。
起動のログに「No encryption key found」が出る
戻した先に config が無く、N8N_ENCRYPTION_KEY も無いと、n8n は次のような行をログに出して新しい鍵を作ります。最後の部分には、その n8n のフォルダの config のパスが入ります。
No encryption key found - Auto-generating and saving to: /path/to/.n8n/config
この鍵では、控えの中の認証情報は読めません。n8n を止め、控えておいた config を置き直してから起動します。逆に、config の鍵と N8N_ENCRYPTION_KEY が食い違うと、「Mismatching encryption keys.」で始まるエラーで起動しません。どちらかにそろえます。
まとめ
- n8n 2.19.2 は SQLite を WAL モードで開くので、確定したばかりの中身が
-walにしか無いことがあります。 - 動いている n8n のデータベースを
cpで写すと、壊れた控えになることがあります。integrity_checkのokは保証になりません。 - 控えは
.backupかVACUUM INTOで取り、書き出し先は絶対パスで書きます。cpは n8n を止めて-walが消えてからです。 - 戻すときは n8n を止め、古い
-walと-shmを退避してから置き換えます。 - 暗号鍵の入った
~/.n8n/configも控え、別のマシンにも置きます。 - Docker では
docker stopの待ち時間の既定が短いので、止めたあと-walが残っていないかを見ます。
参考にした公式ドキュメント
- Write-Ahead Logging(SQLite)
- How To Corrupt An SQLite Database File(SQLite)
- Using the SQLite Online Backup API(SQLite)
- Online Backup API(SQLite)
- VACUUM(SQLite)
- Pragma statements(SQLite)
- Command Line Shell For SQLite(SQLite)
- Database|環境変数の一覧(n8n Docs)
- Docker でのインストール(n8n Docs)
- docker container stop(Docker Docs)
- Tilde Expansion(Bash Reference Manual)
- Expansion(zsh)
n8nの点検と構築のご相談
「アップグレード前の控えで本当に戻せるか不安」「n8nのデータベースの控えを毎日自動で取りたい」といったn8nの運用の点検や、控えと戻し方の仕組みづくりをお引き受けしています。






