n8nをセルフホストで導入する:Docker ComposeでVPSに立て、独自ドメインとHTTPSで使う

公開 書いた人 大林 諒

n8nは自分のサーバーに入れて動かせます。ここでは手元のPCで試す方法と、月額1,000〜2,000円台のVPSにDocker Composeで本番を立て、n8n.example.co.jp のような独自ドメインとHTTPSで使うまでを順に書きます。読み終わると、問い合わせフォームからのWebhookを受けられる本番のn8nが1台立ち、暗号鍵・タイムゾーン・バックアップ・証明書の4つで後から困らない状態になります。前提はLinuxのサーバーにSSHで入ってコマンドを打てることです。版は執筆時点の安定版 2.44.7 で確かめています。

この記事の内容
  1. 先に決めること
  2. 立て方と環境変数の一覧
  3. 手元のPCで試す
  4. VPSに本番を立てる
  5. 立てたら最初にやること
  6. 応用
  7. できないことと、その代わり
  8. よく詰まるところ
  9. 次にやること

先に決めること

手元で試すだけか、本番で使うか

手元のn8nはPCを閉じれば止まり、その間はスケジュールもWebhookも動きません。操作を覚える段階なら手元で十分です。「問い合わせ受付」ワークフローのように業務を載せるなら、最初から24時間動くサーバーに立てます。試すだけならn8n Cloudのトライアルを使う手もあり、費用の比べ方は「n8nの料金:Cloudとセルフホストの費用、商用利用の線引き」に書いています。

データベースはSQLiteかPostgreSQLか

何も設定しなければSQLiteです。ワークフロー・Credential(接続情報)・実行履歴が database.sqlite という1つのファイルに入ります。公式は、試すにはSQLiteで十分、数人以上で使う・一日中ワークフローが動く本番ではPostgreSQLを、と勧めています。

社員30名ほどで、ワークフロー10本前後・触る人2〜3人ならSQLiteでも回ります。ただし後から替えると書き出して入れ直す手間が出るので、触る人が増える見込みがあるなら最初からPostgreSQLにします。この記事はSQLiteで立て、応用で切り替え方を書きます。

サーバーの大きさ

公式はn8nはCPUよりメモリが効くとし、メモリ320MB〜2GB、DBは512MB〜4GBのSSDという例を示しています。問い合わせ受付のような軽い処理ならメモリ2GBのVPSで足ります。

一方、n8n Assistant(組み込みのAIの手伝い機能)用のサンドボックスまで載せる公式の新しいCompose構成は、メモリ4GB・2vCPU以上が要ります。安いVPSでは、この記事のTraefik版で本体だけを立てます。WordPress用の共用レンタルサーバーはDockerが動かないので使えません。AWSのEC2でも手順は同じで、ファイアウォールがセキュリティグループという別画面にあるのが違いです。

独自ドメインとHTTPSが要る理由

1つ目はWebhookです。フォームやSlackからn8nを呼んでもらうには、インターネットから届くURLが要ります。n8nはそのURLをWebhookノードに表示し、外のサービスにも登録するので、ここがlocalhostだと外からは届きません。

2つ目はOAuthのコールバックです。GoogleスプレッドシートやGmailのCredentialでは、認証後の戻り先(OAuth Redirect URL)をGoogle側に登録します。手元なら http://localhost:5678/rest/oauth2-credential/callback で通りますが、本番のURLに移ると登録し直しです。最初から本番のURLで作れば1回で済みます。

URLはサブドメインにします。example.co.jp/n8n/ のようなパスはリバースプロキシと相性が悪いと公式にあります。

立て方と環境変数の一覧

立て方向いている場面外からのWebhook
npx n8n手元で今すぐ触る。Node.js 24系が必要届かない
docker run手元で試し、データも残す届かない
Docker Compose+TraefikVPSの本番。証明書の更新も自動届く
Docker Compose+nginx+certbot既にnginxが動いているVPS届く

本番で押さえる環境変数です。

環境変数例の値意味
N8N_HOSTn8n.example.co.jpホスト名。既定は localhost
N8N_PROTOCOLhttps外から見たプロトコル。既定は http
N8N_WEBHOOK_URLhttps://n8n.example.co.jp/Webhookの外向きURL。旧名 WEBHOOK_URL は2.35.0から非推奨
N8N_PROXY_HOPS1前に立つリバースプロキシの数
N8N_EDITOR_BASE_URLhttps://n8n.example.co.jp/編集画面の公開URL。n8nが送るメールにも使う
GENERIC_TIMEZONEAsia/Tokyon8nのタイムゾーン。既定は America/New_York
TZAsia/TokyoコンテナのOSの時刻
N8N_ENCRYPTION_KEYランダムな値Credentialの暗号鍵。未設定なら初回起動時に自動生成
DB_TYPEpostgresdbPostgreSQLを使うとき。未設定ならSQLite
N8N_RUNNERS_MODEexternalCodeノードを別コンテナで動かす
N8N_RUNNERS_AUTH_TOKENランダムな値n8nと実行用コンテナの合言葉

手元のPCで試す

npxで動かす

n8n 2.44.7 のパッケージが求めるNode.jsは24以上です(公式ドキュメントの記載は20.19〜24.x なので、24系を入れます)。

  1. Node.jsの24系を入れる
  2. node -v が v24 で始まることを確かめる
  3. npx n8n を実行する
  4. ブラウザで http://localhost:5678 を開き、オーナーアカウントを作る

npmで入れたn8nではn8n Assistantが使えず、npmでの導入は3.0で廃止予定です。長く使うならDockerにします。

docker runで動かす

Mac・WindowsはDocker Desktopを入れます。WindowsはWSL2の上で動かし、作業フォルダは /mnt/c/ の下ではなくWSLの中に置きます。

docker volume create n8n_data
docker run -it --rm \
 --name n8n \
 -p 5678:5678 \
 -e GENERIC_TIMEZONE="Asia/Tokyo" \
 -e TZ="Asia/Tokyo" \
 -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
 -v n8n_data:/home/node/.n8n \
 n8nio/n8n:2.44.7

データは n8n_data ボリュームに残り、同じコマンドで続きから使えます。GoogleはOAuthの戻り先にlocalhostを認めるので、スプレッドシートとつなぐ練習もできます。

localhost:5678 で最初に出るオーナーアカウントの登録画面
localhost:5678 で最初に出るオーナーアカウントの登録画面

VPSに本番を立てる

公式のDocker Compose+Traefik版に本番用の設定を足した形で書きます。Traefikは証明書の取得と更新を自動でやるリバースプロキシです。

1. VPSを借りてファイアウォールを開ける

  1. メモリ2GB以上のVPSをUbuntuで借りる
  2. SSHの鍵を登録し、IPアドレスを控える
  3. VPSのファイアウォールで22番・80番・443番を開ける
  4. 5678番は開けない

2. DockerとDocker Composeを入れる

Dockerの公式手順どおりに入れ、確かめます。sudo なしで使うなら作業ユーザーを docker グループに入れます。

docker --version
docker compose version
sudo usermod -aG docker ${USER}
exec sg docker newgrp

3. DNSにAレコードを足す

ドメインを買ったところの管理画面で、Aレコードを足します。

種類名前値
An8nVPSのIPアドレス

dig +short n8n.example.co.jp でVPSのIPが返るまで待ちます。反映前に起動すると、Traefikの証明書の取得が失敗します。

4. 作業フォルダと .env を作る

mkdir n8n-compose
cd n8n-compose
mkdir local-files
openssl rand -hex 32
openssl rand -hex 32

local-files はファイルをやり取りする場所で、権限を正しくするため手で作ります。openssl の2つの値は暗号鍵と合言葉に使います。.env は次のとおりです。GENERIC_TIMEZONE を書かないとニューヨーク時間になり、SSL_EMAIL には証明書の案内が届きます。

DOMAIN_NAME=example.co.jp
SUBDOMAIN=n8n
GENERIC_TIMEZONE=Asia/Tokyo
SSL_EMAIL=admin@example.co.jp
N8N_ENCRYPTION_KEY=1つ目の値
N8N_RUNNERS_AUTH_TOKEN=2つ目の値

鍵が入るので chmod 600 .env で権限を絞り、Gitには入れません。

5. compose.yaml を作る

公式のTraefik版に、本番で要るものを # 追加 の印付きで足しています。n8nの版は 2.44.7 に固定し、Codeノード実行用の n8nio/runners も同じ版にそろえます(公式の決まりです)。

services:
  traefik:
    image: "traefik"
    restart: always
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
      - "--certificatesresolvers.mytlschallenge.acme.email=${SSL_EMAIL}"
      - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - traefik_data:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro

  n8n:
    image: n8nio/n8n:2.44.7
    restart: always
    ports:
      - "127.0.0.1:5678:5678"
    labels:
      - traefik.enable=true
      - traefik.http.routers.n8n.rule=Host(`${SUBDOMAIN}.${DOMAIN_NAME}`)
      - traefik.http.routers.n8n.tls=true
      - traefik.http.routers.n8n.entrypoints=web,websecure
      - traefik.http.routers.n8n.tls.certresolver=mytlschallenge
      - traefik.http.middlewares.n8n.headers.SSLRedirect=true
      - traefik.http.middlewares.n8n.headers.STSSeconds=315360000
      - traefik.http.middlewares.n8n.headers.browserXSSFilter=true
      - traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
      - traefik.http.middlewares.n8n.headers.forceSTSHeader=true
      - traefik.http.middlewares.n8n.headers.SSLHost=${DOMAIN_NAME}
      - traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
      - traefik.http.middlewares.n8n.headers.STSPreload=true
      - traefik.http.routers.n8n.middlewares=n8n@docker
    environment:
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - N8N_WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
      - N8N_EDITOR_BASE_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/  # 追加
      - N8N_PROXY_HOPS=1  # 追加
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      - TZ=${GENERIC_TIMEZONE}
      - N8N_RESTRICT_FILE_ACCESS_TO=/files
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}  # 追加
      - N8N_RUNNERS_MODE=external  # 追加
      - N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0  # 追加
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}  # 追加
    volumes:
      - n8n_data:/home/node/.n8n
      - ./local-files:/files

  task-runners:  # 追加
    image: n8nio/runners:2.44.7
    restart: always
    environment:
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}  # 追加
      - TZ=${GENERIC_TIMEZONE}  # 追加
    depends_on:
      - n8n

volumes:
  n8n_data:
  traefik_data:

N8N_PROXY_HOPS は公式がリバースプロキシの後ろでは 1 にするよう書いている設定です。task-runners は、Codeノードのコードをn8n本体と別のコンテナで動かすためのものです。公式は、分けない内部モードだとワークフローを編集できる人が暗号鍵やCredentialまで読めるおそれがあるとして、本番では外部モードを勧めています。Codeノードはこの task-runners 側で動くので、タイムゾーンもこちらに渡します。n8n本体にだけ書くと、Codeノードの中の $now はニューヨーク時間、new Date() は世界標準時のままです。

6. 起動する

sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs -f traefik

3つのコンテナが running になれば起動しています。

7. オーナーアカウントを作る

  1. https://n8n.example.co.jp を開き、鍵のマークが出ていることを確かめる
  2. メールアドレス・名前・パスワードを入れる(8文字以上、数字と大文字を1つ以上)
  3. 「Next」(次へ)を押す

最初にこの画面を通った人がオーナーになるので、起動したらすぐ作ります。社員の招待は、左下のユーザーアイコン横の「…」→「Settings」→「Users」→「Invite」です。SMTPを設定しないと招待リンクは手で渡すことになり、パスワードの再設定もできません。

8. 外から届くかを確かめる

  1. 新しいワークフローにWebhookノードを置く
  2. ノードを開き、「Webhook URLs」の「Production URL」タブに出るURL(本番用URL)が https://n8n.example.co.jp/webhook/ で始まることを確かめる
  3. curl -s https://n8n.example.co.jp/healthz が応答することを確かめる
Webhookノードを開き、「Production URL」タブを選んだところ。Production URL が https://n8n.example.co.jp/webhook/ で始まっている
Webhookノードを開き、「Production URL」タブを選んだところ。Production URL が https://n8n.example.co.jp/webhook/ で始まっている

nginx+certbotで立てる別解

既にnginxが動いているVPSなら、compose.yaml から traefik と labels を外し、nginxを前に立てます。n8nは画面の更新にWebSocketを使うので、Upgrade を転送しないと画面が固まります。

server {
    listen 443 ssl;
    server_name n8n.example.co.jp;
    ssl_certificate     /etc/letsencrypt/live/n8n.example.co.jp/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.example.co.jp/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

この形の弱点は、証明書の更新を自分で仕込まないと誰も更新しないことです。取るときに更新後のnginx読み直しを指定し、予行演習まで通します。

sudo certbot certonly --webroot -w /var/www/html -d n8n.example.co.jp --deploy-hook "systemctl reload nginx"
sudo certbot renew --dry-run

立てたら最初にやること

暗号鍵を控える

n8nはCredentialを暗号鍵で暗号化してDBに保存します。鍵をなくすと、DBのバックアップがあってもCredentialは復号できず、スプレッドシート・Gmail・Slackの接続を全部作り直しです。この手順では鍵を .env に書いたので、.env をサーバーの外に控えます。鍵を指定せずに立てた環境では、自動で作られた鍵がボリュームの .n8n/config に入っているので、それを控えます。後から N8N_ENCRYPTION_KEY を足すときは、config と同じ値を入れます。

タイムゾーンを日本にする

GENERIC_TIMEZONE がないとn8nはニューヨーク時間で動き、「毎朝9時に未対応件数をSlackへ」が日本時間の夜22時か23時に届きます。TZ はコンテナのOSの時刻で、ログや date の結果が日本時間になります。上の構成ではCodeノードは task-runners で動くため、そちらにも GENERIC_TIMEZONE と TZ が要ります。2.44.7で試すと、ランナーに渡さない場合はCodeノードの $now がニューヨーク時間、new Date() が世界標準時になり、ランナーに足すとどちらも日本時間に揃いました。ワークフローごとにも、画面上部のワークフロー名の横の「…」から「Settings」を開き、「Timezone」で上書きでき、そちらが優先されます。

ワークフロー名の横の「…」から開いた「Workflow settings」。Timezoneの欄が見えているところ
ワークフロー名の横の「…」から開いた「Workflow settings」。Timezoneの欄が見えているところ

バックアップを取る

SQLiteなら、ボリュームの .n8n フォルダにDB・暗号鍵・設定が全部入っています。公式は書き込み中のコピーは壊れうるので、止めてからコピーするよう書いています。

sudo docker compose stop n8n
sudo docker run --rm -v n8n-compose_n8n_data:/data:ro -v $(pwd)/backup:/backup alpine tar czf /backup/n8n-$(date +%Y%m%d).tar.gz -C /data .
sudo docker compose start n8n

ボリューム名の頭には作業フォルダ名が付くので、docker volume ls で確かめます。

実際に本番で動かしていて、2つ困りました。1つは、DBファイルだけをコピーしたら直近の変更が抜けていたことです。2.xのSQLiteは既定でWALモードで動き(2.44.7で database.sqlite-wal と -shm ができるのを確かめています)、本体に書き戻される前の変更が横の database.sqlite-wal にあるため、本体だけを開くと数分前に直したワークフローが古いままでした。-wal -shm も一緒に、上のようにフォルダごと固めれば抜けません。

もう1つは、実行履歴が溜まってDBが1.3GBほどになった環境で、docker cp での取り出しに10分以上かかり、別の日には1時間以上進まずに止まったことです。自動で回すコピーには timeout 1200 のような打ち切りを付けています。DBを太らせないよう、履歴を残す期間と件数も絞ります。

      - EXECUTIONS_DATA_MAX_AGE=168
      - EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000

履歴の削除は既定で有効(14日・10,000件)ですが、SQLiteのファイルは縮みません。縮めるには DB_SQLITE_VACUUM_ON_STARTUP=true を入れ、業務時間外に再起動します。

証明書の期限を外から見張る

nginx+certbotで立てていた本番で、証明書の期限が切れ、n8nからの通知が全部止まったことがあります。最初の発行をコマンド1回で済ませ、更新を仕込んでいませんでした。毎朝nginxを読み直す設定だけがあり、更新されているつもりになっていました。

通知はn8n自身のWebhookをHTTPSで呼ぶ形だったので、期限切れの瞬間に全部失敗し、同じ経路のエラー通知も出ませんでした。処理自体は成功していましたが、気づくまで数日かかりました。

直したのは3つです。更新とnginxの読み直しを週2回のcronで回す。出力をログに残す。残り日数をサーバーの外の別のPCから毎朝確かめ、30日を切ったら知らせる。監視をn8nの中に置くと、n8nと一緒に止まります。

echo | openssl s_client -connect n8n.example.co.jp:443 -servername n8n.example.co.jp 2>/dev/null | openssl x509 -noout -enddate

certbotの本番の更新は5分以上かかることがあり、途中で止めたらロックが残って以降のcertbotが黙って固まりました。

アップデートの基本

compose.yaml のn8nとrunnersの版を同じ番号に書き換え、sudo docker compose pull、sudo docker compose down、sudo docker compose up -d の順に打ちます。公式は月1回以上の更新とリリースノートの確認を勧めています。戻し方や脆弱性の追い方は「n8nを止めずに運用する:アップデート・バックアップ・エラー通知・脆弱性」に書いています。

応用

PostgreSQLに替える

新しく立てるなら、compose.yaml に公式の例どおり postgres:18 のサービスとデータ用ボリュームを足し、n8nの environment に次を足します。

      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}

PostgreSQL側の PGDATA: /var/lib/postgresql/data の行は消しません。18はデータの置き場所の既定が変わったので、この行がないとボリュームの外に書かれ、作り直したときDBが空になります。PostgreSQLにしても暗号鍵は .n8n に入るので、そのボリュームは残します。

SQLiteの本番から替えるときは、n8n export:workflow --backup と n8n export:credentials --backup で書き出し、新しい側でオーナーを作ってから import で入れ直します。ユーザーや実行履歴は移らず、ワークフローは止まった状態で入ります。

できないことと、その代わり

できないこと代わりの手
セルフホストでは「Sign in with Google」(Managed OAuth2)が使えないGoogle Cloud ConsoleでOAuthクライアントを作り、Client IDとClient Secretを入れる
npmで入れるとn8n Assistantが使えない。npm導入は3.0で廃止予定Dockerで入れる
止まっている間に来たWebhookや、過ぎたスケジュールは取り戻せない外から死活を見張る。フォーム側でも送信内容をメールで控える
SQLiteからPostgreSQLへの自動移行はないCLIで書き出して入れ直す
CLIのバックアップにはユーザー・実行履歴・変数・設定が入らない.n8n のボリュームごと残す
安いVPSにAssistantのサンドボックスまでは載らない本体だけ立てる。使うならメモリ4GB以上にする

Googleの「Testing」状態で利用者の種類が「External」のOAuthアプリは、同意とトークンが7日で切れます。社内のGoogle Workspaceだけで使うなら「Internal」にします。

よく詰まるところ

症状原因直し方
WebhookのURLが http://localhost:5678/ で始まるN8N_WEBHOOK_URL がない、または変更後に作り直していない設定して docker compose up -d
Google連携で redirect_uri_mismatchGoogle側の戻り先とOAuth Redirect URLが違う(http/https、ポート)Credential画面のURLをそのまま貼り直す
開くと応答がない80・443が閉じているファイアウォールで許可する
証明書が「TRAEFIK DEFAULT CERT」で警告が出るDNS未反映・向き先違い・443が閉じているdig で向き先を見て、docker compose logs traefik を読む
ファイルを読み書きするノードで権限エラーlocal-files をrootで作ったdocker compose exec n8n id の番号で chown する
スケジュールが夜中に動くGENERIC_TIMEZONE がないAsia/Tokyo にして作り直す
Codeノードの日時がずれるtask-runners にタイムゾーンを渡していないtask-runners にも GENERIC_TIMEZONE と TZ を足す
作り直したらCredentialが全部使えないボリュームを消した、または鍵が変わった控えた鍵を N8N_ENCRYPTION_KEY に戻す
IPアドレスで開くとログインできないログインのCookieはHTTPSでしか送られない独自ドメインのHTTPSで開く
起動し直したらワークフローが消えたdown -v でボリュームを消した、またはフォルダ名を変えた-v を付けない。フォルダ名を戻す
Webhookのパスが使用中で公開できない同じパスとメソッドが別のワークフローにあるパスかメソッドを変える
nginx経由で編集画面が固まるWebSocketが転送されていないUpgrade と Connection を転送する

次にやること

あわせて読む