n8n Queue ModeをDocker Composeで構成する

n8n Queue ModeをDocker Composeで構成する記事のサムネイル

個人用の自動化基盤として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 ModeWorker受付と実行を分離でき、Workerを増やして処理を分散できる

Queue ModeでRedisに入るのは、ワークフロー本体ではなく「どの実行を処理するか」というジョブ情報です。ワークフロー定義、Credential、実行履歴などはPostgreSQLに保存されるため、MainとすべてのWorkerが同じデータベースを参照する必要があります。

実行時の流れ

  1. MainがWebhook、Trigger、または手動実行を受け付ける
  2. Mainが実行ジョブをRedisへ登録する
  3. 空いているWorkerがRedisからジョブを取得する
  4. WorkerがPostgreSQLから必要な情報を読み、ワークフローを実行する
  5. Code Nodeを使う処理は、Workerに紐づくTask Runnerで実行する
  6. 実行結果や履歴をPostgreSQLへ保存する

複数のWorkerを起動している場合、特定のWorkerを固定して使うのではなく、キューからジョブを取得できたWorkerが実行を担当します。この仕組みによって、Workerの台数を増やすことで同時に処理できる実行数を広げられます。

Queue Modeにしただけでは無停止構成にはならない

ここは少し紛らわしいところですが、Queue Modeは実行処理を分離・分散する仕組みであって、それだけでn8n全体が高可用性になるわけではありません。

  • Mainが1台なら、更新中はUIやWebhookの受付が一時的に止まる
  • Redisが停止すると、新しいジョブの受け渡しができない
  • PostgreSQLが停止すると、MainとWorkerの両方が処理を続けられない
  • MainとWorkerでは、同じデータベースと暗号化キーを共有する必要がある

今回の構成も完全な無停止構成ではありません。目的は、MainとWorkerの役割を分け、Worker側を片方ずつ更新できるようにすることです。

今回作った構成

構成としてはこんな感じです。

n8n Queue Mode構成図。Caddy、n8n Main、Redis、PostgreSQL、2組のWorkerとTask Runnerの関係を示す。
n8n Queue Mode構成図。MainはUIとWebhookを担当し、Redisを介して2台のWorkerへ処理を渡します。各Workerは専用のTask Runnerを持ちます。画像をタップすると原寸で表示できます。

役割はざっくり以下です。

コンテナ役割
n8n-mainUI、Webhook、Trigger管理
n8n-workerワークフロー実行
n8n-worker-22つ目のWorker
n8n-worker-runnerCode Node実行用
n8n-worker-runner-22つ目のWorker用
redisQueue Mode用のキュー
postgresn8nのデータ保存
caddyHTTPS化

ポイントは、n8n本体でワークフローを直接実行せず、Workerに処理を投げるところです。

.env
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 を作業ディレクトリに用意します。

Shell
touch .env

必要なシークレットを作ります。

Shell
openssl rand -hex 32

.env の以下を置き換えます。

.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なら、以下のようにまとめて置き換えできます。

Shell
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 '' にします。

Shell
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もコンテナで起動します。

Shell
docker compose --profile local-db up -d

状態確認。

Shell
docker compose ps

n8nにアクセスします。

URL
http://localhost:5678

ヘルスチェックも見ておきます。

Shell
curl -s http://localhost:5678/healthz

正常ならこんな感じです。

JSON
{"status":"ok"}

docker-compose.ymlの中身

全体をそのまま掲載するのではなく、Queue Modeを構成するうえで重要な部分を抜粋します。以下は2026年6月14日時点のstable版であるn8n 2.25.7を基準に確認しています。

n8n共通設定

MainとWorkerで同じ設定を使うので、YAML anchorでまとめています。

YAML
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を受ける役目です。

YAML
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の待受アドレス、共有トークンを設定します。

YAML
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は同じ認証トークンを共有します。

YAML
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イメージとコンテナ制限を適用します。

YAML
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: ALLLinux capabilityを落とす
tmpfs: /tmp/tmp だけ一時領域として使う

個人環境でも、Code Nodeを使うならこのあたりは気にしておきたいところです。

Code Nodeで使えるモジュールを制限する

外部モードでは、n8n-task-runners.jsontask-runners 配列にJavaScriptとPythonのRunner設定をまとめます。各言語で利用を許可するモジュールだけを明示します。

JSON
{
  "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": ""
      }
    }
  ]
}

許可リストはモジュールをインストールする設定ではありません。lodashmomentaxios などを使う場合は、それらを追加した独自のRunnerイメージをビルドし、そのうえで許可リストへ登録する必要があります。

Redis

Redisは実行IDをWorkerへ受け渡すメッセージブローカーです。ワークフロー定義や実行結果の永続データはPostgreSQLに保存されます。

YAML
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はコンテナ間通信だけで使うので、基本的にはポート公開しません。

YAML
# ports:
#   - "6380:6379"

デバッグしたいときだけ一時的に開ける想定です。

PostgreSQL

ローカル検証ではPostgreSQLも一緒に起動します。

YAML
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 のホストを変えます。

.env
DB_POSTGRESDB_HOST=<外部PostgreSQLのホスト名>
DB_POSTGRESDB_SSL_ENABLED=true

個人検証ではコンテナDBから手軽に始められます。長期運用では、バックアップや可用性を考えてRDSなどのマネージドDBへ移行する選択肢もあります。

CaddyでHTTPS化する

本番ではCaddyを使う想定です。

Caddyfile はかなりシンプルにしました。

JSON
{
    email {$ACME_EMAIL:}
}
{$DOMAIN_NAME:localhost} {
    reverse_proxy n8n-main:5678
}

.env はこんな感じにします。

.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

起動。

Shell
docker compose --profile production up -d

ローカルDBも一緒に使うならこうです。

Shell
docker compose --profile production --profile local-db up -d

ログ確認

問題が起きたときは、まず docker compose ps で各コンテナの状態を確認し、続いて全体または n8n-main、Worker、Redis、PostgreSQLのログを個別に追います。

Shell
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 でバージョンを管理しています。

.env
N8N_VERSION=2.25.7

普通に更新するなら以下です。

Shell
docker compose pull
docker compose up -d

Workerは片方ずつ停止・更新します。N8N_GRACEFUL_SHUTDOWN_TIMEOUT を300秒に設定し、停止要求を受けたWorkerが実行中ジョブの完了を待てる時間を確保しています。ただし、上限時間を超える長時間ジョブまで無停止を保証する手順ではありません。

Shell
# 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を使う場合は、そのサービスのスナップショットやバックアップ機能を利用します。

Shell
docker compose exec postgres pg_dump -U n8n n8n_cluster > backup.sql

リストア。

Shell
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が扱えなくなります。

.env
N8N_ENCRYPTION_KEY=...

n8nとRunnerのバージョンを揃える

n8n本体とRunnerは同じバージョンにします。

.env
N8N_VERSION=2.25.7

Queue Modeではバイナリデータの保存先に注意する

Queue ModeではMainとWorkerが同じバイナリデータへアクセスできる必要があり、ローカルの filesystem モードは利用できません。今回は database モードでPostgreSQLに保存します。

.env
N8N_DEFAULT_BINARY_DATA_MODE=database

Code Nodeから環境変数を読ませない

Code Nodeから機密情報を読めると怖いので、ブロックしています。

.env
N8N_BLOCK_ENV_ACCESS_IN_NODE=true

Community Packagesは無効にした

今回はコミュニティノードのインストールも無効にしています。

.env
N8N_COMMUNITY_PACKAGES_ENABLED=false

独自ノードは ./n8n/custom にマウントする想定です。

YAML
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は十分に活用できます。今後は実際の運用を続けながら、監視やバックアップ、更新手順もさらに整えていきたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です