n8nをDockerで起動し、UIを開かずCLIだけでワークフローを実行する

公開:

本記事には広告(PR)が含まれます。

セルフホストのn8nを、CIやGUIのないリモートサーバーなどブラウザを開けない環境で自動実行したい人向けの記事。UIを開かずにどこまでの操作がCLIで完結するかを、Docker上のn8n 2.35.4で実測した。

結論

n8nをDockerで起動し、UIを一切開かずにワークフローのインポートから実行、実行結果JSONの取得までがCLIだけで完結することを実機で確認した。

やったことと結果は次のとおり。すべてDocker上のn8n 2.35.4での実測である。

やったこと結果
新規ボリュームでコンテナを起動healthz応答まで約7秒
healthz直後にワークフローをインポート成功。エディタ表示の準備完了(15.5秒)を待つ必要はない
n8n executeでワークフローをCLI実行成功。1回目4,616ms、2回目159ms(初回はGitHub API側の応答待ち)
実行結果の取得標準出力のJSONにノード別実行時間と最終データがすべて入る
タスクブローカーのポート競合を切り分けN8N_RUNNERS_BROKER_PORTの変更だけで解消。N8N_RUNNERS_ENABLED=falseは不要

つまずいたのは、ワークフローJSONにidと各ノードのpositionが必須である点と、タスクブローカーのポート競合の2点で、いずれも回避手順を本文に示す。

検証環境

  • n8n: 2.35.4(イメージ n8nio/n8n:2.35.4にタグ固定。docker exec n8n-verify n8n --version2.35.4で確認)
  • 実行環境: セルフホスト(Docker Desktop、Docker Engine 28.3.3、Windows 11 Home)
  • シェル: Git Bash(curlはcurl.exe)。PowerShellではcurlInvoke-WebRequestのエイリアスのため、本記事のコマンドはそのままでは動かない
  • 検証日: 2026-08-20 JST。本文のログのタイムスタンプはUTC表記

n8nをDockerで起動してCLIでワークフローを実行する手順

1. ワークフローJSONを用意して保存

Manual Trigger → HTTP Request(GitHub APIでn8nリポジトリ情報を取得) → Edit Fields(repo名とスター数を抽出)の3ノード構成。github-repo-check.jsonとしてカレントディレクトリに保存する。

{
  "id": "VerifyGithub0001",
  "name": "github-repo-check",
  "nodes": [
    { "parameters": {}, "name": "Manual Trigger", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [0, 0] },
    { "parameters": { "url": "https://api.github.com/repos/n8n-io/n8n", "options": {} }, "name": "HTTP Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [200, 0] },
    { "parameters": { "assignments": { "assignments": [ { "name": "repo", "value": "={{ $json.full_name }}", "type": "string" }, { "name": "stars", "value": "={{ $json.stargazers_count }}", "type": "number" } ] }, "options": {} }, "name": "Edit Fields", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [400, 0] }
  ],
  "connections": { "Manual Trigger": { "main": [[ { "node": "HTTP Request", "type": "main", "index": 0 } ]] }, "HTTP Request": { "main": [[ { "node": "Edit Fields", "type": "main", "index": 0 } ]] } },
  "active": false,
  "settings": {}
}

ここで2点つまずいた。最上位にidを付けずにインポートすると次のエラーで失敗する。

An error occurred while importing workflows. See log messages for details.
SQLITE_CONSTRAINT: NOT NULL constraint failed: workflow_entity.id

また各ノードにpositionを付けないと次のエラーで失敗する。

An error occurred while importing workflows. See log messages for details.
Workflow structure is invalid. nodes[0].position (invalid_type): Required; nodes[1].position (invalid_type): Required; nodes[2].position (invalid_type): Required

上記JSONは両方とも修正済みの形。

2. docker runで起動し、待つ間にdocker cpでファイルをコピーする

docker cpはn8nの起動完了を待たずに実行できるため、起動直後にコピーを済ませてからhealthzのポーリングに入る。実際に実行した一連のコマンドは次のとおり。

docker run -d --name n8n-verify -p 5678:5678 -v n8n_verify_data:/home/node/.n8n n8nio/n8n:2.35.4
s=$(date +%s.%N)
docker cp github-repo-check.json n8n-verify:/tmp/github-repo-check.json
while :; do
  ts=$(date +%s.%N)
  h=$(curl -s --max-time 1 -o /dev/null -w "%{http_code}" http://localhost:5678/healthz)
  awk -v t="$ts" -v r="$s" -v h="$h" 'BEGIN{printf "+%.2fs healthz=%s\n", t-r, h==""?"000":h}'
  [ "$h" = "200" ] && break
  sleep 0.5
done
curl -s http://localhost:5678/healthz

出力。docker runはコンテナIDのみを出力し、docker cpは成功時に何も出力しない。経過時間の起点はdocker run完了直後で、初回ポーリングが+2.47sから始まるのはdocker cpの所要時間による。

0ac2201c84fac6d5290f2355ba1a2964b756a44b443a564370ae74b31266c131
+2.47s healthz=000
+3.75s healthz=000
+5.02s healthz=000
+5.76s healthz=000
+6.52s healthz=200
{"status":"ok"}

起動時刻とn8n側のログは次のコマンドで確認できる。

docker inspect -f '{{.State.StartedAt}}' n8n-verify
docker logs -t n8n-verify | grep -E "n8n ready|Editor is now accessible"

出力:

2026-08-19T18:05:15.801687449Z
2026-08-19T18:05:22.891873743Z n8n ready on ::, port 5678
2026-08-19T18:05:31.314518666Z Editor is now accessible via:

コンテナのStartedAt起点で、主要なイベントまでの経過時間は次のとおり。

  • n8n readyログまで7.1秒
  • Editor is now accessibleログまで15.5秒
  • docker run完了はStartedAtの0.6秒後
  • healthz 200の観測(docker run完了起点で+6.52s)は、StartedAt起点では約7.1秒に相当し、n8n readyログとほぼ同時

新規ボリュームの初回起動ではDBマイグレーションが走る。総数は次のコマンドで確認できる。

docker logs n8n-verify 2>&1 | grep -c "Starting migration"

出力:

234

なお、この秒数とマイグレーション挙動は新規ボリューム前提の実測である。再実行する場合はdocker rm -f n8n-verifydocker volume rm n8n_verify_dataで作り直すこと。次の手順のとおり、インポートはhealthz 200の直後から可能だった。

3. n8n import:workflowでワークフローをインポートする

docker exec n8n-verify n8n import:workflow --input=/tmp/github-repo-check.json

出力:

Importing 1 workflows...
Successfully imported 1 workflow.

このインポートはhealthzが200を返した直後(起点+6.8秒)に開始し、+12.5秒時点で完了した。開始・完了の経過時間は手順2と同一スクリプト内で続けて計測したものである。手順2でファイルをコピー済みなら、Editor is now accessible(StartedAt起点15.5秒)より前でも、healthzが200を返せばインポートは通ることを実測で確認した。

4. 同一コンテナでn8n executeを実行するとport 5679 is already in useで失敗する

環境変数を付けずにn8n executeを実行すると、常駐プロセスがタスクブローカーの5679番ポートを使用中のため失敗する。

docker exec n8n-verify n8n execute --id=VerifyGithub0001

出力:

n8n Task Broker's port 5679 is already in use. Do you have another instance of n8n running already?

N8N_RUNNERS_ENABLED=falseだけを付けても同じエラーで失敗する。

docker exec -e N8N_RUNNERS_ENABLED=false n8n-verify n8n execute --id=VerifyGithub0001

出力:

n8n Task Broker's port 5679 is already in use. Do you have another instance of n8n running already?

5. N8N_RUNNERS_BROKER_PORTでポート競合を解消してCLI実行する

N8N_RUNNERS_BROKER_PORTでブローカーのポートを変更すると、これだけで成功する。N8N_RUNNERS_ENABLED=falseは不要。

docker exec -e N8N_RUNNERS_BROKER_PORT=5680 n8n-verify n8n execute --id=VerifyGithub0001

実行に成功すると、出力冒頭にn8n Task Broker ready on 127.0.0.1, port 5680が出てポート変更が効いたことが確認でき、Execution was successful:に続けて実行結果のJSON全体が標準出力に出る。別コマンドを実行する必要はない。JSON内の主な構造は次のとおり。

  • data直下にはversionstartDataがあり、resultDataの後にはexecutionDataresumeTokenが続く
  • ノード別の実行時間・開始終了時刻・最終データはdata.resultData.runData以下にノード名ごとに格納されている
  • runDataの各要素の先頭にはstartTimeexecutionIndexsourcehintsがあり、data内には各ノードの出力本体が入る

以下は2回目の実行の抜粋で、...は省略箇所を示す。

n8n Task Broker ready on 127.0.0.1, port 5680
(中略)
Execution was successful:
====================================
{
  "data": {
    ...,
    "resultData": {
      "runData": {
        "Manual Trigger": [
          { ..., "executionTime": 2, "executionStatus": "success", ... }
        ],
        "HTTP Request": [
          { ..., "executionTime": 109, "executionStatus": "success", ... }
        ],
        "Edit Fields": [
          {
            ...,
            "executionTime": 40,
            "executionStatus": "success",
            "data": { "main": [[ { "json": { "repo": "n8n-io/n8n", "stars": 201192 }, ... } ]] }
          }
        ]
      },
      "lastNodeExecuted": "Edit Fields"
    },
    ...
  },
  "mode": "cli",
  "startedAt": "2026-08-19T18:06:40.715Z",
  "stoppedAt": "2026-08-19T18:06:40.874Z",
  "storedAt": "db",
  "status": "success",
  "finished": true
}

比較のため、1回目の実行の抜粋も同じ形で示す。所要時間4,616msの大半がHTTP Requestノードの4,562msで、初回の遅さがGitHub API側の応答待ちによるものと内訳から分かる。

n8n Task Broker ready on 127.0.0.1, port 5680
(中略)
Execution was successful:
====================================
{
  "data": {
    ...,
    "resultData": {
      "runData": {
        "Manual Trigger": [
          { ..., "executionTime": 1, "executionStatus": "success", ... }
        ],
        "HTTP Request": [
          { ..., "executionTime": 4562, "executionStatus": "success", ... }
        ],
        "Edit Fields": [
          { ..., "executionTime": 42, "executionStatus": "success", ... }
        ]
      },
      "lastNodeExecuted": "Edit Fields"
    },
    ...
  },
  "mode": "cli",
  "startedAt": "2026-08-19T18:06:31.724Z",
  "stoppedAt": "2026-08-19T18:06:36.340Z",
  "storedAt": "db",
  "status": "success",
  "finished": true
}

実行時にFailed to start Python task runner ... Python 3 is missingという警告が出るが、JSのみで構成したワークフローはこの警告と無関係にsuccessで完走する。

起動時間とCLI実行時間の実測結果

  • 起動(新規ボリューム): StartedAt起点でn8n readyまで7.1秒、Editor is now accessibleまで15.5秒。healthz 200の観測はdocker run完了起点で+6.52s(StartedAt起点の約7.1秒に相当)で、readyログとほぼ同時
  • healthz 200直後のインポート: docker run完了起点で+6.8秒に開始し成功(+12.5秒に完了)。事前にdocker cpを済ませておけば、Editor is now accessibleを待たなくても通る
  • CLI実行1回目: startedAt 18:06:31.724Z、stoppedAt 18:06:36.340Z(4,616ms)。内訳はManual Trigger 1ms、HTTP Request 4,562ms、Edit Fields 42ms(合計4,605ms)で、全体との差11msがワークフロー前後の処理。初回の遅さはGitHub API側の応答によるもの
  • CLI実行2回目: startedAt 18:06:40.715Z、stoppedAt 18:06:40.874Z(159ms)。内訳はManual Trigger 2ms、HTTP Request 109ms、Edit Fields 40ms(合計151ms)で、全体との差8msがワークフロー前後の処理
  • 実行結果: {"repo": "n8n-io/n8n", "stars": 201192}
  • ポート競合の切り分け: 環境変数なしとN8N_RUNNERS_ENABLED=falseのみはどちらも「port 5679 is already in use」で失敗し、N8N_RUNNERS_BROKER_PORT=5680のみで成功した。必要なのはブローカーポートの変更のみで、N8N_RUNNERS_ENABLED=falseは単独では効果がない

CLI実行の結果をUIの実行履歴で確認する

ここまでの手順はUIを一切開かずに完結しているが、CLI実行の結果はDBに保存されており(実行出力JSONのstoredAtdb)、後からエディタ画面でも確認できる。以下のスクリーンショットは、本記事と同一の手順を同じ検証環境で再実行して取得したもので、本文に引用したログとは別の試行である。この試行の所要時間は1回目1.358秒、2回目532msで、引用した試行の4,616ms・159msとは一致しない。所要時間はGitHub API側の応答に左右されるため、初回が遅く2回目が速い傾向だけが再現する。また画面の時刻表示はブラウザのローカル時刻(JST)で、本文ログのUTC表記とは9時間ずれる。

UIの閲覧は検証の本筋ではないため手順には含めない。http://localhost:5678/への初回アクセス時のみオーナーアカウントの作成を求められ、メールアドレス・氏名・パスワードを登録する。以下は作成を済ませた状態の画面で、ワークフロー一覧からgithub-repo-checkを開くとJSONで定義した3ノードが表示され、画面上部のExecutionsタブに切り替えると実行履歴が見える。

CLIでインポートしたワークフローをエディタで開いた状態。JSONで定義した3ノードがposition指定どおりに並んでいる

Executionsタブの実行履歴。2件が成功として記録されている

この試行でこのワークフローを実行したのはCLIからの2回のみで、履歴の2件はそれに対応する。同一手順の再実行のため、この2件の実行も本文に引用した実行出力JSONと同じくmodeはcliである。

まとめ

  • n8nはDockerコンテナ上でn8n executeを使えば、UIを開かずインポートから実行、結果の取得までCLIだけで完結する
  • 起動待ちはhealthzのポーリングで足り、healthzが返ればインポートは通る
  • ワークフローJSONにはidと各ノードのpositionが必須
  • 同一コンテナ内でCLI実行する場合は、タスクブローカーのポート競合をN8N_RUNNERS_BROKER_PORTで回避する
  • ノード別の実測値や最終データは実行出力のJSONにすべて含まれており、別途取得コマンドを実行する必要はない

出典