VPSにn8nをDockerとHTTPSで構築する方法
VPS上でn8nをDocker Compose、Postgres、リバースプロキシで構築します。WEBHOOK_URLと暗号化キーの落とし穴、よくあるエラー文字列をまとめて解説します。
構築するもの
n8n はワークフロー自動化ツールです。トリガー、Webhook、スケジュール、フォーム送信を起点に、API の呼び出し、データの変換、他のシステムへの書き込みを行うノードの連鎖を、ビジュアルエディターで構築できます。サービスを自分で実装しなくても、あらゆるモデルプロバイダーやデータベースと連携できるため、AI エージェントのワークフローをつなぐ標準的なツールになっています。1 つの docker run で、2 分以内に使用可能なエディターを構築できます。このガイドでは、残りの 90% を扱います。デフォルトの SQLite ファイルではなく Postgres で永続化し、HTTPS 経由でアクセスできるようにします。そして、ほとんどの人が誤る点として、外部から実際に到達できる URL を Webhook が提供するようにします。
完成した構成では、1 つの Docker ネットワーク上で 2 つのコンテナを稼働させます。n8n 本体と、ワークフローおよび認証情報を保持する Postgres データベースです。ホスト上のリバースプロキシが TLS を終端し、localhost 上の n8n に転送します。そのため、インターネットに直接公開されるのはこのプロキシ経由の接続だけです。他のサービスとともに、2026 年版セルフホスティング候補一覧 に含まれます。
前提条件と、現実的な制約
最低 1 GB の RAM を搭載した VPS が必要です。ワークフローが実際の処理を行うようになったら、2 GB を見込んでください。実行処理と Node.js ランタイムがメモリを消費するためです。実行中に OOM killer がコンテナを停止し、その時点で初めて容量不足に気付くのは避けるべきです。最初は 1 vCPU で問題ありません。
このサーバーでより負荷の高いサービスも実行する場合は、先にそのサービスに合わせて容量を決めてください。一般的な原因はフォトライブラリです。PhotoPrism と Immich に実際に必要な RAM の下限は、n8n が要求する容量を大きく上回ります。
メディアサーバーも同様です。Jellyfin と、そのライブラリを閲覧するためのフロントエンド(ライブラリを 90 年代のレンタルショップのように再構成する Halcyonなど)を実行すると、n8n が気付くよりはるかに早く RAM とトランスコード用の余裕を消費します。
ドメインまたはサブドメインが必要です。ここでは n8n.example.com とします。VPS のパブリック IP を指す A レコードを設定し、証明書を要求する前に名前解決できる状態にしてください。ポート 80 と 443 はプロキシからアクセスできるように開放する必要があります。n8n 自身のポート 5678 はインターネットに公開してはいけません。Docker Engine と Compose plugin も必要です。docker compose version が docker: 'compose' is not a docker command でエラーになる場合は、古い standalone binary を使用しています。plugin は sudo apt install docker-compose-plugin です。
SQLite はテスト用途なら十分ですが、依存する用途には Postgres を使用します
n8n のデフォルトデータベースは /home/node/.n8n/database.sqlite にある SQLite ファイルです。動作確認だけなら問題ありません。ボリュームをマウントしなければ、最初にコンテナを再作成した時点でデータは失われます。これ自体が学ぶべき点です。Postgres に移行する理由は、単純な速度ではありません。SQLite は書き込みロックを 1 つしか保持できないため、複数のワークフローを同時に実行するインスタンスや、将来使うことになるキューモードでは、並行処理時に SQLITE_BUSY: database is locked が発生します。Postgres にはこの上限がなく、pg_dump で適切にバックアップできます。また、依存するサーバーには Postgres を使う前提で n8n 自身のドキュメントが説明されています。後から切り替える場合は手作業でデータを移行する必要があります。このサーバーが重要であれば、最初から Postgres を使用してください。
DNS とファイアウォール
レコードを設定し、先にポートを開放してください。後続の証明書取得で、名前解決できない名前を指定して失敗するのを防げます。
dig +short n8n.example.com
curl -s ifconfig.me
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow OpenSSH
sudo ufw enable5678 は開放しないでください。compose ファイルは n8n を 127.0.0.1:5678 にバインドするため、ホスト上のリバースプロキシだけが接続できます。ufw allow 5678 を設定すると、この分離が失われます。
Compose ファイル
作業ディレクトリを作成し、docker-compose.yml を配置します。これがスタック全体です。サービスは 2 つ、プライベートネットワークは 1 つ、名前付きボリュームは 2 つです。
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- n8n_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:2.29.10
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_PROXY_HOPS=1
- GENERIC_TIMEZONE=Europe/London
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- n8n_data:/home/node/.n8n
networks:
- n8n_net
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:
networks:
n8n_net:明示しておくべき点がいくつかあります。DB_POSTGRESDB_HOST=postgres は サービス名 です。Docker はこれを共有ネットワーク上で解決します。localhost ではありません。n8n コンテナ内では、localhost は n8n 自体を意味します。depends_on と condition: service_healthy により、起動時に n8n が Postgres より先に起動しようとする競合を防ぎます。これらがないと、n8n は起動後にデータベースを見つけられず、終了します。/home/node/.n8n にある名前付きボリューム n8n_data には、暗号化キーが保存されます。SQLite を使用する場合はデータベースも保存されます。ここは絶対に失わないでください。イメージは正確なバージョンに固定し、latest は決して使用しないでください。理由は、この後のアップグレードのセクションで説明します。
シークレットファイル
compose ファイルにパスワードを記述しないでください。compose が自動的に読み込む .env ファイルを同じディレクトリに配置し、実際にランダムな値になるよう生成してください。
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
printf 'N8N_ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
chmod 600 .envN8N_ENCRYPTION_KEY は、ここで最も重要な文字列です。保存されるすべての認証情報は、このキーで暗号化されます。n8n に生成させず、明示的に設定してください。自分で生成した値なら、記録して復元できます。n8n がこのキーで最初の認証情報を暗号化した後に 変更すると、すべての認証情報を復号できなくなります。そのため、今ここで 1 回だけ設定し、この行は二度と変更しないでください。
Webhook の動作を決める環境変数
4 つの変数が、n8n が外部に対して自身をどのように示すかを制御します。これらを誤って設定することが、n8n のサポートで最も多い問題です。
N8N_HOSTは公開ホスト名です。n8n.example.com。プロキシの背後でデフォルトのlocalhostのままにすると、エディターは ブラウザー内でlocalhostから自身の API を読み込もうとするため、失敗します。N8N_PROTOCOL=httpsは、n8n が TLS 経由で提供されていることを示します。そのため、セッション Cookie にSecureを付け、https://URL を生成します。N8N_PORT=5678は、コンテナ内で n8n が待ち受けるポートです。公開ポートではありません。443 はプロキシが管理します。WEBHOOK_URL=https://n8n.example.com/が特に問題になりやすい変数です。n8n は、これらの値から Webhook アドレスを生成し、Stripe、GitHub、その他の外部呼び出し元に貼り付ける URL として表示します。未設定または誤っている場合、n8n はN8N_HOST:N8N_PORTにフォールバックし、https://n8n.example.com:5678/webhook/...、さらに悪い場合はhttp://localhost:5678/webhook/...をエラーなしで提示します。これらの URL は一見正しそうに見えますが、インターネットから到達できないため、呼び出し元のリクエストが無言で届かなくなります。末尾のスラッシュを含む正確な公開ベース URL を設定し、Webhook ノードにポート番号のない URL が表示されることを確認してください。
N8N_PROXY_HOPS=1 は、n8n の Express サーバーに対し、前段に 1 つのプロキシがあることを信頼するよう指示します。これにより、レート制限やクライアント IP を読み取る機能が、プロキシのアドレスではなく実際のアドレスを認識できます。ここで意図的に設定しない変数が N8N_RUNNERS_ENABLED です。タスクランナー、つまり n8n が Code ノードの処理を別のサンドボックス化されたプロセスで実行する方式は、1.69 以降デフォルトになっており、このガイドが固定する 2.x 系列では必須です。そのため、従来のオプトイン設定は非推奨です。今設定すると、n8n は削除するよう通知をログに出力するだけです。
初回起動
docker compose up -d
docker compose ps
docker compose logs -f n8n正常な初回起動では、n8n ready on ..., port 5678 の行に続いて Editor is now accessible via: の行が表示されます。docker compose ps には Up の両方のコンテナが表示され、postgres は (healthy) とマークされている必要があります。n8n が Restarting ループに陥っている場合はログを確認してください。ほとんどの場合、原因は後述するデータベース接続またはボリュームの権限です。
リバースプロキシでの TLS
n8n 自体は 5678 でプレーン HTTP を使用します。HTTPS の終端は前段のコンポーネントが担います。構成は 2 つあります。
すでに複数のコンテナを運用している場合は、TLS 証明書を自動発行する Traefik リバースプロキシの背後に n8n を配置し、いくつかのラベルを設定します。Traefik が証明書の取得と更新を行います。
このサーバーで運用するアプリが n8n だけであれば、Let's Encrypt 証明書を使用する nginx のバーチャルホスト構成が簡単です。証明書を取得するには、Ubuntu 24.04 で Certbot と nginx の TLS を設定する手順を使用します。その後、次の server ブロックを設定します。
server {
listen 443 ssl;
server_name n8n.example.com;
ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/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-Proto $scheme;
proxy_read_timeout 3600;
client_max_body_size 16m;
}
}Upgrade と Connection "upgrade" のヘッダーは省略できません。n8n は WebSocket 経由でエディターに実行状況の更新をリアルタイムで送信します。この 2 行がないと、ログインページの読み込み後に接続が失われたことを示すバナーが表示され、処理が停止します。proxy_read_timeout 3600 により、長時間実行される処理が nginx のデフォルト設定である 60 秒後に切断されるのを防ぎます。X-Forwarded-Proto $scheme ヘッダーは N8N_PROXY_HOPS=1 と組み合わせて使用します。プロキシから n8n への接続がプレーン HTTP であっても、元のリクエストが HTTPS だったことを n8n に伝えます。これにより、n8n は接続が安全でないと判断して自身の Cookie を拒否しません。
最初のワークフローを実際に動かす
https://n8n.example.com/を開き、所有者アカウントを作成し(次のセクションを参照)、経路が機能することを確認できる最小構成のワークフローを作成します。Webhook を受け取り、HTTP リクエストを実行し、レスポンスを返す構成です。
- Webhook ノードを追加します。メソッドを
POSTに設定し、パスにはhelloのような値を指定します。ここには Test URL と Production URL の 2 つの URL が表示されます。これは「Webhook が動作しない」という報告の主な原因です。Test URL は、Listen for test event をクリックしている間に限り、1 回のリクエストに応答します。その後は有効期限が切れます。Production URL は、ワークフローが Active の間、常に応答します。 - HTTP Request ノードを追加し、Webhook ノードの後に接続します。任意の公開 JSON API を指定します。
https://api.github.com/zenへの GET リクエストは 1 行の文字列を返すため、これで十分です。 - Respond to Webhook ノードを追加します。Webhook ノードの Respond オプションを「Using Respond to Webhook node」に設定し、HTTP ノードの出力を呼び出し元へ返すようにします。
- 右上でワークフローを Active に切り替え、
curl -X POST https://n8n.example.com/webhook/helloを呼び出します。zen の文が返ります。POST で受信し、API を呼び出し、レスポンスを返すという、実際の自動化の多くで使われる構成です。
スケジュール実行にする場合は、Webhook ノードを Schedule Trigger に置き換え、代わりにモデルのエンドポイントを呼び出します。同じ VPS 上で動作する Ollama のようなセルフホスト型のモデルを使うと、毎晩実行する要約ワークフローを簡単に構築できます。
ユーザー管理、Basic 認証ではありません
以前の n8n ガイドでは、N8N_BASIC_AUTH_ACTIVE=true を設定するよう案内していました。これらの変数は n8n 1.0 で削除され、現在は何も行いません。現在の認証は owner account です。エディターを初めて開くと、n8n でメールアドレスとパスワードを使う owner account を作成します。この認証ゲートは必須で、匿名モードはありません。初回起動後、URL を他の人に渡す前に、すぐ作成してください。docker compose up から最初のフォーム送信までの間は、最初にアクセスしたユーザーがインスタンスを取得できます。リバースプロキシに basic-auth 層を追加するのは、追加のロックとして妥当です。ただし、これは第 2 の認証要素であり、本来の認証ではありません。このガイドで扱う owner account とその他の機能は、無料の community edition で利用できます。後から細かなロールを持つ追加ユーザーや SSO が必要になる場合は、計画を立てる前に 有料ライセンスが必要な n8n の機能 を確認してください。
バックアップでは、最初に暗号鍵、次にデータベースを確保します
バックアップが必要なものは2つあります。ただし、同じように置き換えられるわけではありません。
N8N_ENCRYPTION_KEYです。 n8n に保存するすべての認証情報、API トークン、データベースパスワード、OAuth シークレットは、この鍵で保存時に暗号化されます。Postgres 内のワークフローは、この鍵がなければ使用できません。別の鍵を使う新しいサーバーにデータベースを復元しても、n8n は認証情報を1つも復号できず、復旧もリセットもできません。.env ファイルにこの鍵が保存されています。作成した当日にサーバー外へコピーしてください。パスワードマネージャーのエントリに保存する方法が適しています。実際に重要なのは、このバックアップです。
Postgres データベースもバックアップします。データベースには、ワークフロー、実行履歴、暗号化された認証情報そのものが保存されています。
docker compose exec -T postgres pg_dump -U n8n -d n8n \
| gzip > n8n-db-$(date +%F).sql.gzこれをスケジュールに従って実行し、ダンプをサーバー外へコピーしてください。新しい VPS に復元する場合は、まずスタックを1回起動してデータベースを作成し、n8n を停止します。psql でダンプを読み込み、同じ N8N_ENCRYPTION_KEY を .env に設定してから、n8n を起動してください。同じ鍵とダンプがあれば動作するインスタンスを復元できます。新しい鍵を使うと、どの認証情報も利用できないワークフローになります。
アップグレード: タグを固定する
compose ファイルでは、意図的に latest ではなく n8nio/n8n:2.29.10 を固定しています。n8n はほとんどの週で新しいマイナーバージョンをリリースし、バージョン間でデータベーススキーマやノードの動作が変更されることがあります。そのため、latest のままでは、無人の pull によって、起動した瞬間にデータベースをマイグレーションするビルドが適用される可能性があります。バージョンを固定し、更新する前に リリースノート を確認してください。n8n は破壊的変更をそこに記載しています。アップグレードは意図的に実施します。
docker compose exec -T postgres pg_dump -U n8n -d n8n | gzip > pre-upgrade.sql.gz
# edit the image tag in docker-compose.yml, then:
docker compose pull n8n
docker compose up -d n8n
docker compose logs -f n8nメジャーバージョンをまたぐ場合に、この点が最も重要になります。たとえば 2.0 系では、デフォルトで N8N_BLOCK_ENV_ACCESS_IN_NODE から true に変更されました。そのため、process.env を読み取る Code ノードは、false に戻すまでアクセスできなくなりますが、これに気付かない可能性があります。同じリリースでは、settings ファイルに対する厳格な権限チェックも適用されるようになりました。メジャーバージョンをまたぐ前に、2.0 の破壊的変更ページ を確認してください。n8n は起動時に必要なデータベースマイグレーションを自動実行します。そのため、アップグレード前の pg_dump は省略できません。認証情報は .env のキーで暗号化して保存され、データは Postgres に保存されるため、コンテナは使い捨てにできます。アップグレードではコンテナを置き換え、ロールバックでは以前のタグを固定してダンプを復元します。
障害パターンと表示される文字列
The requested webhook "POST hello" is not registered. ワークフローが Active ではない状態で webhook を呼び出した場合、または待ち受け中のプロセスがない状態でテスト用パスを呼び出した場合に返される 404 です。テスト用パス (/webhook-test/...) は「Listen for test event」をクリックしている間だけ応答します。本番用パス (/webhook/...) は、ワークフローのトグルがオンの場合だけ応答します。兄弟の This webhook is not registered for GET requests. Did you mean to make a POST request? はメソッドが間違っていることを示します。node は POST を想定しているのに、GET を送信しています。
webhook URL に :5678 または localhost が表示されます。 node には https://n8n.example.com:5678/webhook/... または http://localhost:5678/... が表示されます。WEBHOOK_URL が未設定または誤っているため、n8n は公開用のベース URL ではなく N8N_HOST:N8N_PORT からアドレスを生成しています。WEBHOOK_URL=https://n8n.example.com/ を設定し、docker compose up -d を使ってコンテナを再作成すると、ポートは表示されなくなります。
ブラウザーに There was a problem loading init data が表示されます。 エディターは読み込まれていますが、自身のバックエンド API に接続できていません。プロキシの背後では、ほとんどの場合、N8N_HOST または WEBHOOK_URL の設定誤り、プロキシで WebSocket の Upgrade ヘッダーが転送されていないこと、または N8N_PROTOCOL が接続方法と一致していないことが原因です。公開側の 4 つの変数と、プロキシが Upgrade および Connection を転送していることを確認してください。
ログに password authentication failed for user "n8n" が表示され、コンテナが再起動を繰り返します。 n8n が送信するパスワードと、データベースの初期化時に使われたパスワードが一致していません。注意点は、Postgres が POSTGRES_PASSWORD を読み取るのは、空のデータディレクトリを初期化するときだけだということです。スタックを 1 回起動した後で POSTGRES_PASSWORD を .env 内で変更しても、既存の postgres_data volume には古いパスワードが残っています。元の値に戻してください。保持するデータがない場合は、docker compose down と docker volume rm を実行して postgres volume を削除し、初期状態から起動してください。
起動時に EACCES: permission denied, open '/home/node/.n8n/config' が表示されます。 n8n は node user (UID 1000) として実行されるため、設定ディレクトリに書き込めません。root が所有するホストディレクトリ (./n8n_data:/home/node/.n8n) を bind mount した場合に発生します。前述の named volume を使用してください。bind mount を使う場合は、先に sudo chown -R 1000:1000 ./n8n_data を実行してください。
Permissions 0644 for n8n settings file /home/node/.n8n/config are too wide. Changing permissions to 0600.. 2.x 系以降、n8n はデフォルトでその設定ファイルに 0600 を適用し、起動時に自動修正します。このログ行は、すでにモードを修正したことを示します。bind mount の使用後や、権限が緩い状態でファイルを復元した後によく発生します。対応は不要です。ファイルシステムが実際に権限をサポートできない場合に限り、N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false を設定してください。
Mismatching encryption keys 詳細なログ行には、設定ファイル内の暗号化キー /home/node/.n8n/config が、環境内の N8N_ENCRYPTION_KEY と一致しないと表示されます。環境内のキーが、以前の実行時に n8n が data volume へ書き込んだキーと異なっています。多くの場合、以前の起動時に変数が未設定だったため n8n がランダムなキーを生成し、その後で別のキーを設定したことが原因です。.env に元のキーを戻してください。保存しておく必要がある認証情報が本当にない場合に限り、n8n_data volume 内の config ファイルを削除して n8n に再生成させることもできます。ただし、既存の認証情報は読み取れなくなります。
セキュア Cookie に関するログインバナー: Your n8n server is configured to use a secure cookie, however you are either visiting this via an insecure URL, or using Safari.。N8N_PROTOCOL=https を設定しているのに、通常は IP アドレスとポートを直接指定しているため、n8n に平文 HTTP でアクセスしています。https://n8n.example.com/ 経由でアクセスしてください。HTTPS を本当に使用できない場合に限り N8N_SECURE_COOKIE=false を設定してください。インターネットに公開するサーバーでは絶対に設定しないでください。
これらのワークフロー内に言語モデルを組み込む方法については、Claude と n8n で AI ワークフローを構築する を参照してください。
FAQ
n8n では SQLite と Postgres のどちらを使うべきですか?
SQLite(デフォルト)は、n8n を試す場合や、一度に 1 つのワークフローだけを実行する個人用インスタンスには適しています。依存する処理に使う場合は Postgres に移行してください。SQLite の単一書き込みロックは並行実行時に database is locked を発生させますが、Postgres なら pg_dump で安全にバックアップできます。後からの移行は手動になるため、重要なサーバーで運用するなら最初から Postgres を使ってください。
n8n の Webhook がまったく実行されないのはなぜですか?
ほとんどの場合、WEBHOOK_URL が原因です。未設定または誤った値の場合、n8n は N8N_HOST:N8N_PORT を基に Webhook アドレスを生成します。そこに :5678 や localhost が含まれることが多く、一見有効に見えてもインターネットから到達できないため、呼び出し元のリクエストが届きません。WEBHOOK_URL=https://n8n.example.com/ を設定し、ノードに表示される URL にポートが含まれていないことを確認してください。もう 1 つの原因は、ワークフローを Active に切り替えていない状態で Webhook を呼び出していることです。この場合は The requested webhook ... is not registered. が返されます。
n8n で何をバックアップする必要がありますか?
2 つあります。1 つ目は、.env ファイルにある N8N_ENCRYPTION_KEY です。保存されているすべての認証情報はこのキーで暗号化されているため、失うと永久に復号できなくなります。作成した当日にサーバーの外部へコピーしてください。2 つ目は、ワークフロー、履歴、認証情報を保存する Postgres データベースの pg_dump です。復元には、同じキーとダンプの両方が必要です。
n8n を HTTPS の背後に配置するにはどうすればよいですか?
n8n はポート 5678 で平文の HTTP を提供し、前段のリバースプロキシが TLS を終端します。プロキシだけが接続できるよう、n8n を 127.0.0.1:5678 にバインドしてください。そのうえで、自動証明書取得に対応した Traefik、または Let's Encrypt 証明書を使う nginx を使用します。N8N_PROTOCOL=https と WEBHOOK_URL=https://your-host/ を設定し、プロキシが WebSocket の Upgrade ヘッダーを転送することを確認してください。転送されないとエディターが応答しなくなります。
n8n を安全にアップグレードするにはどうすればよいですか?
latest ではなく、特定のイメージタグを固定してください。n8n は起動時に自動でマイグレーションを実行するため、最初に pg_dump を取得します。破壊的変更についてリリースノートを確認し、その後タグを更新して docker compose pull n8n && docker compose up -d n8n を実行してください。コンテナは使い捨てにできるため、以前のタグを固定し、アップグレード前のダンプを復元すればロールバックできます。