個人用の自動化基盤としてn8nを使い続けるうちに、ワークフローやCode Nodeが増え、受付処理と実行処理を分けて運用したくなりました。
そこで今回は、n8nをMain、Worker、Task Runner、Redis、PostgreSQLに分け、Docker ComposeでQueue Modeを構成しました。役割分離の考え方から主要な設定、起動確認、更新、バックアップまでをまとめます。
掲載するComposeや設定は構成の要点を示す抜粋です。特定の非公開リポジトリを前提にせず、環境に合わせて組み立てられるよう、各設定が必要な理由もあわせて説明します。
Queue Modeとは
通常構成のn8nでは、UIやWebhookを受け付けるプロセスが、そのままワークフローの実行も担当します。
Queue Modeでは、この「受付」と「実行」を分離します。Mainはワークフローを直接実行せず、実行ジョブをRedisのキューへ登録します。Workerはキューからジョブを取得し、実際のワークフローを実行します。
通常モードとの違い
| 構成 | ワークフローの実行担当 | 特徴 |
|---|---|---|
| 通常モード | n8n本体 | 構成がシンプルで、小規模な利用に向いている |
| Queue Mode | Worker | 受付と実行を分離でき、Workerを増やして処理を分散できる |
Queue ModeでRedisに入るのは、ワークフロー本体ではなく「どの実行を処理するか」というジョブ情報です。ワークフロー定義、Credential、実行履歴などはPostgreSQLに保存されるため、MainとすべてのWorkerが同じデータベースを参照する必要があります。
実行時の流れ
- MainがWebhook、Trigger、または手動実行を受け付ける
- Mainが実行ジョブをRedisへ登録する
- 空いているWorkerがRedisからジョブを取得する
- WorkerがPostgreSQLから必要な情報を読み、ワークフローを実行する
- Code Nodeを使う処理は、Workerに紐づくTask Runnerで実行する
- 実行結果や履歴をPostgreSQLへ保存する
複数のWorkerを起動している場合、特定のWorkerを固定して使うのではなく、キューからジョブを取得できたWorkerが実行を担当します。この仕組みによって、Workerの台数を増やすことで同時に処理できる実行数を広げられます。
Queue Modeにしただけでは無停止構成にはならない
ここは少し紛らわしいところですが、Queue Modeは実行処理を分離・分散する仕組みであって、それだけでn8n全体が高可用性になるわけではありません。
- Mainが1台なら、更新中はUIやWebhookの受付が一時的に止まる
- Redisが停止すると、新しいジョブの受け渡しができない
- PostgreSQLが停止すると、MainとWorkerの両方が処理を続けられない
- MainとWorkerでは、同じデータベースと暗号化キーを共有する必要がある
今回の構成も完全な無停止構成ではありません。目的は、MainとWorkerの役割を分け、Worker側を片方ずつ更新できるようにすることです。
今回作った構成
構成としてはこんな感じです。

役割はざっくり以下です。
| コンテナ | 役割 |
|---|---|
n8n-main | UI、Webhook、Trigger管理 |
n8n-worker | ワークフロー実行 |
n8n-worker-2 | 2つ目のWorker |
n8n-worker-runner | Code Node実行用 |
n8n-worker-runner-2 | 2つ目のWorker用 |
redis | Queue Mode用のキュー |
postgres | n8nのデータ保存 |
caddy | HTTPS化 |
ポイントは、n8n本体でワークフローを直接実行せず、Workerに処理を投げるところです。
EXECUTIONS_MODE=queue
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
これで、UIやWebhookを受ける n8n-main と、実際にワークフローを実行する n8n-worker を分離できます。
なぜQueue Modeにしたのか
個人利用でも、ワークフローを継続的に増やしていくなら、MainとWorkerを分けるメリットがあります。
ただ、n8nを使っていると、だんだん次のようなことが気になってきます。
ワークフロー実行中に再起動したくない
n8nをアップデートしたいときや、設定を変えたいときに、ワークフロー実行中だと少し気になります。
Queue ModeにしてWorkerを分けておくと、少なくとも「UIを持っているMain」と「実行担当のWorker」を分けられます。
Code Nodeを少し隔離したい
n8nのCode Nodeは便利です。
ただ、便利な分だけ、なんでもできてしまいます。
そのため、Code Nodeの実行はTask Runner側に分けて、少しでもn8n本体から切り離したいと思いました。
Workerを増やす構成を試したかった
最初から大きな負荷があるわけではありません。
ただ、Workerを複数にしたときにどう動くのかを知っておきたかったので、2Worker構成にしています。
.envを作る
Composeから参照する .env を作業ディレクトリに用意します。
touch .env
必要なシークレットを作ります。
openssl rand -hex 32
.env の以下を置き換えます。
N8N_ENCRYPTION_KEY=CHANGE_ME_RUN_openssl_rand_hex_32
N8N_RUNNERS_AUTH_TOKEN=CHANGE_ME_RUN_openssl_rand_hex_32
POSTGRES_PASSWORD=CHANGE_ME_RUN_openssl_rand_hex_32
REDIS_PASSWORD=CHANGE_ME_RUN_openssl_rand_hex_32
Linuxなら、以下のようにまとめて置き換えできます。
sed -i "s/N8N_ENCRYPTION_KEY=.*/N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/N8N_RUNNERS_AUTH_TOKEN=.*/N8N_RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)/" .env
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(openssl rand -hex 32)/" .env
macOSの場合は sed -i '' にします。
sed -i '' "s/N8N_ENCRYPTION_KEY=.*/N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)/" .env
sed -i '' "s/N8N_RUNNERS_AUTH_TOKEN=.*/N8N_RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)/" .env
sed -i '' "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i '' "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$(openssl rand -hex 32)/" .env
N8N_ENCRYPTION_KEY はかなり重要です。
これをなくすとCredentialが復号できなくなるので、ちゃんと保管しておきます。
ローカルで起動する
ローカルではPostgreSQLもコンテナで起動します。
docker compose --profile local-db up -d
状態確認。
docker compose ps
n8nにアクセスします。
http://localhost:5678
ヘルスチェックも見ておきます。
curl -s http://localhost:5678/healthz
正常ならこんな感じです。
{"status":"ok"}
docker-compose.ymlの中身
全体をそのまま掲載するのではなく、Queue Modeを構成するうえで重要な部分を抜粋します。以下は2026年6月14日時点のstable版であるn8n 2.25.7を基準に確認しています。
n8n共通設定
MainとWorkerで同じ設定を使うので、YAML anchorでまとめています。
x-n8n-shared: &n8n-shared
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION:-2.25.7}
restart: unless-stopped
environment: &n8n-env
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST:-postgres}
DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT:-5432}
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:-n8n_cluster}
DB_POSTGRESDB_USER: ${POSTGRES_USER:-n8n}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:?N8N_ENCRYPTION_KEY is required}
EXECUTIONS_MODE: queue
N8N_GRACEFUL_SHUTDOWN_TIMEOUT: "300"
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: "6379"
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
Queue Modeなので、Redisを使います。
また、MainとWorkerで N8N_ENCRYPTION_KEY は必ず同じ値にする必要があります。
n8n-main
n8n-main はUIとWebhookを受ける役目です。
n8n-main:
<<: *n8n-shared
ports:
- "${N8N_PORT:-5678}:5678"
environment:
<<: *n8n-env
WEBHOOK_URL: ${WEBHOOK_URL:-http://localhost:5678/}
N8N_HOST: ${N8N_HOST:-localhost}
N8N_PROTOCOL: ${N8N_PROTOCOL:-http}
N8N_PORT: "5678"
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS を有効にして、手動実行もWorker側に逃がすようにしました。
Workerを2つ用意する
Workerは2つにしています。
Task Runnerを外部モードで接続するため、Worker側ではRunnerの有効化、外部モード、Brokerの待受アドレス、共有トークンを設定します。
x-n8n-worker-env: &n8n-worker-env
<<: *n8n-env
N8N_RUNNERS_ENABLED: "true"
N8N_RUNNERS_MODE: external
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:?N8N_RUNNERS_AUTH_TOKEN is required}
n8n-worker:
<<: *n8n-shared
command: worker
environment:
<<: *n8n-worker-env
n8n-worker-2:
<<: *n8n-shared
command: worker
environment:
<<: *n8n-worker-env
Workerを2つにしている理由は、並列実行というよりローリングアップデートを試したかったからです。
片方ずつ更新できると、少し安心です。
Task RunnerもWorkerごとに用意する
Code Node用にTask Runnerを使います。
Queue Modeで外部Task Runnerを使う場合、公式ドキュメントではWorkerごとに専用のsidecarを持たせる構成が案内されています。WorkerとRunnerは同じ認証トークンを共有します。
x-runner-env: &runner-env
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:?N8N_RUNNERS_AUTH_TOKEN is required}
n8n-worker-runner:
<<: *runner-shared
environment:
<<: *runner-env
N8N_RUNNERS_TASK_BROKER_URI: http://n8n-worker:5679
depends_on:
- n8n-worker
n8n-worker-runner-2:
<<: *runner-shared
environment:
<<: *runner-env
N8N_RUNNERS_TASK_BROKER_URI: http://n8n-worker-2:5679
depends_on:
- n8n-worker-2
WorkerとRunnerを1対1で持たせています。
docker compose --scale でもWorkerを増やせそうですが、Runnerとの紐づきが分かりづらくなるので、今回は明示的に定義しました。
このあたりは好みが分かれそうです。
Runnerは少し固める
Code Nodeの実行環境をn8n本体から分離し、Runner側には公式が推奨するdistrolessイメージとコンテナ制限を適用します。
x-runner-shared: &runner-shared
image: n8nio/runners:${N8N_VERSION:-2.25.7}-distroless
restart: unless-stopped
user: "65532:65532"
read_only: true
cap_drop:
- ALL
tmpfs:
- /tmp
やっていることは以下です。
| 設定 | 内容 |
|---|---|
user: "65532:65532" | nobodyユーザーで実行 |
read_only: true | ファイルシステムを書き込み不可にする |
cap_drop: ALL | Linux capabilityを落とす |
tmpfs: /tmp | /tmp だけ一時領域として使う |
個人環境でも、Code Nodeを使うならこのあたりは気にしておきたいところです。
Code Nodeで使えるモジュールを制限する
外部モードでは、n8n-task-runners.json の task-runners 配列にJavaScriptとPythonのRunner設定をまとめます。各言語で利用を許可するモジュールだけを明示します。
{
"task-runners": [
{
"runner-type": "javascript",
"env-overrides": {
"NODE_FUNCTION_ALLOW_BUILTIN": "crypto,fs,path,url,util",
"NODE_FUNCTION_ALLOW_EXTERNAL": "lodash,moment,axios"
}
},
{
"runner-type": "python",
"env-overrides": {
"PYTHONPATH": "/opt/runners/task-runner-python",
"N8N_RUNNERS_STDLIB_ALLOW": "json,math,datetime,re,collections,itertools",
"N8N_RUNNERS_EXTERNAL_ALLOW": ""
}
}
]
}
許可リストはモジュールをインストールする設定ではありません。lodash、moment、axios などを使う場合は、それらを追加した独自のRunnerイメージをビルドし、そのうえで許可リストへ登録する必要があります。
Redis
Redisは実行IDをWorkerへ受け渡すメッセージブローカーです。ワークフロー定義や実行結果の永続データはPostgreSQLに保存されます。
redis:
image: redis:8-alpine
restart: unless-stopped
command: >
redis-server
--requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
--maxmemory ${REDIS_MAXMEMORY:-256mb}
--maxmemory-policy noeviction
--appendonly yes
--appendfsync everysec
個人的に気をつけたのは以下です。
- パスワード必須
- ホストには公開しない
noeviction- AOF有効化
Redisはコンテナ間通信だけで使うので、基本的にはポート公開しません。
# ports:
# - "6380:6379"
デバッグしたいときだけ一時的に開ける想定です。
PostgreSQL
ローカル検証ではPostgreSQLも一緒に起動します。
postgres:
image: postgres:17-alpine
restart: unless-stopped
profiles:
- local-db
environment:
POSTGRES_DB: ${POSTGRES_DB:-n8n_cluster}
POSTGRES_USER: ${POSTGRES_USER:-n8n}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
volumes:
- postgres_data:/var/lib/postgresql/data
外部DBを使う場合は、.env のホストを変えます。
DB_POSTGRESDB_HOST=<外部PostgreSQLのホスト名>
DB_POSTGRESDB_SSL_ENABLED=true
個人検証ではコンテナDBから手軽に始められます。長期運用では、バックアップや可用性を考えてRDSなどのマネージドDBへ移行する選択肢もあります。
CaddyでHTTPS化する
本番ではCaddyを使う想定です。
Caddyfile はかなりシンプルにしました。
{
email {$ACME_EMAIL:}
}
{$DOMAIN_NAME:localhost} {
reverse_proxy n8n-main:5678
}
.env はこんな感じにします。
N8N_PROTOCOL=https
N8N_HOST=n8n.example.com
WEBHOOK_URL=https://n8n.example.com/
DOMAIN_NAME=n8n.example.com
ACME_EMAIL=admin@example.com
N8N_SECURE_COOKIE=true
起動。
docker compose --profile production up -d
ローカルDBも一緒に使うならこうです。
docker compose --profile production --profile local-db up -d
ログ確認
問題が起きたときは、まず docker compose ps で各コンテナの状態を確認し、続いて全体または n8n-main、Worker、Redis、PostgreSQLのログを個別に追います。
docker compose ps
docker compose logs -f
docker compose logs -f n8n-main
docker compose logs -f n8n-worker
docker compose logs -f redis
docker compose logs -f postgres
Workerをローリングアップデートする
n8nとRunnerは同じバージョンに揃えます。
.env でバージョンを管理しています。
N8N_VERSION=2.25.7
普通に更新するなら以下です。
docker compose pull
docker compose up -d
Workerは片方ずつ停止・更新します。N8N_GRACEFUL_SHUTDOWN_TIMEOUT を300秒に設定し、停止要求を受けたWorkerが実行中ジョブの完了を待てる時間を確保しています。ただし、上限時間を超える長時間ジョブまで無停止を保証する手順ではありません。
# Worker 2を停止してからRunnerを止める
docker compose stop -t 300 n8n-worker-2
docker compose stop n8n-worker-runner-2
docker compose pull n8n-worker-2 n8n-worker-runner-2
docker compose up -d n8n-worker-2 n8n-worker-runner-2
docker compose ps n8n-worker-2 n8n-worker-runner-2
# Worker 1も同じ手順で更新
docker compose stop -t 300 n8n-worker
docker compose stop n8n-worker-runner
docker compose pull n8n-worker n8n-worker-runner
docker compose up -d n8n-worker n8n-worker-runner
# 最後にMainを更新
docker compose pull n8n-main
docker compose up -d n8n-main
Mainを更新するとUIやWebhookは短時間止まります。
更新後は docker compose ps とログを確認し、WorkerがRedisとPostgreSQLへ再接続できてから次のWorkerへ進みます。Mainは1台のため、Main更新中はUIとWebhook受付が短時間停止します。
バックアップ
Compose内の postgres コンテナを使う場合、PostgreSQLのバックアップは以下です。外部のマネージドDBを使う場合は、そのサービスのスナップショットやバックアップ機能を利用します。
docker compose exec postgres pg_dump -U n8n n8n_cluster > backup.sql
リストア。
docker compose exec -T postgres psql -U n8n n8n_cluster < backup.sql
n8nではCredentialもDBに入っています。
そのため、DBだけでなく N8N_ENCRYPTION_KEY も必ず保管しておきます。
ここを忘れると、バックアップがあってもCredentialが復号できません。
ハマりそうなところ
N8N_ENCRYPTION_KEYは全コンテナで同じにする
これは大事です。
MainとWorkerで違う値になるとCredentialが扱えなくなります。
N8N_ENCRYPTION_KEY=...
n8nとRunnerのバージョンを揃える
n8n本体とRunnerは同じバージョンにします。
N8N_VERSION=2.25.7
Queue Modeではバイナリデータの保存先に注意する
Queue ModeではMainとWorkerが同じバイナリデータへアクセスできる必要があり、ローカルの filesystem モードは利用できません。今回は database モードでPostgreSQLに保存します。
N8N_DEFAULT_BINARY_DATA_MODE=database
Code Nodeから環境変数を読ませない
Code Nodeから機密情報を読めると怖いので、ブロックしています。
N8N_BLOCK_ENV_ACCESS_IN_NODE=true
Community Packagesは無効にした
今回はコミュニティノードのインストールも無効にしています。
N8N_COMMUNITY_PACKAGES_ENABLED=false
独自ノードは ./n8n/custom にマウントする想定です。
volumes:
- ./n8n/custom:/home/node/.n8n/custom:ro
参考にした公式ドキュメント
使ってみた感想
実際に構築してみると、Queue Modeは単にWorkerを増やすための機能ではなく、UIやWebhookの受付、ワークフローの実行、Code Nodeの処理、ジョブの受け渡し、データの保存を、それぞれ適した役割へ分ける仕組みだと実感できました。
特に良かったのは、次の点です。
- MainとWorkerを分けることで、受付処理と実行処理の境界が明確になった
- Code NodeをTask Runnerへ分離し、実行環境を個別に制限できた
- RedisとPostgreSQLがQueue Modeで担う役割を、実際の動作と結び付けて理解できた
- Workerを複数用意することで、処理の分散や片方ずつ更新する流れを確認できた
- 構成全体をDocker Composeで管理し、同じ環境を再現しやすくできた
役割が明確になったことで、問題が起きたときも「受付」「実行」「キュー」「データベース」のどこを確認すべきか判断しやすくなりました。これは日々の運用だけでなく、n8nや各コンポーネントを更新するときにも役立ちます。
現時点で大きな負荷がなくても、ワークフローを継続的に増やしていくなら、最初から拡張先が見える構成にしておく価値があります。Queue Modeを実際に動かしたことで、n8nを長く育てていくための土台を作れました。
まとめ
今回は、n8nをQueue Modeで動かし、Main、Worker、Task Runner、Redis、PostgreSQLを役割ごとに分けた構成を作りました。
完全な無停止構成ではありませんが、ワークフローの受付と実行を分離し、Workerの追加や更新を行いやすい土台になっています。各コンポーネントの役割も明確になるため、運用中の状態確認や問題の切り分けにも取り組みやすくなりました。
個人環境でも、ワークフローを継続的に育てていく構成としてQueue Modeは十分に活用できます。今後は実際の運用を続けながら、監視やバックアップ、更新手順もさらに整えていきたいと思います。
