SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

n8n VPS 自托管:Docker、Postgres 与 HTTPS 配置

在 VPS 上用 Docker Compose 部署 n8n、Postgres 和 HTTPS 反向代理,详解 WEBHOOK_URL、加密密钥、5678 端口暴露及常见错误字符串。

构建内容

n8n 是一个工作流自动化工具:它提供可视化编辑器,由触发器、webhook、计划任务或表单提交触发一系列节点;这些节点调用 API、重塑数据,并将数据写入其他系统。由于无需编写服务即可连接每个模型提供商和数据库,n8n 已成为 AI agent 工作流的默认 glue。一个 docker run 可在两分钟内获得可用的编辑器。本指南关注其余90%的工作:使用 Postgres 代替默认的 SQLite 文件,使其可通过 HTTPS 访问;此外,还有几乎所有人都会配置错误的一点:让 webhook 提供一个外部世界确实可以访问的 URL。

最终的服务栈包含同一 Docker 网络上的两个容器:n8n 本身,以及用于保存工作流和凭据的 Postgres 数据库。主机上的反向代理终止 TLS,并将请求转发到 localhost 上的 n8n,因此除了该代理之外,没有任何组件直接面向互联网。它与 2026 年自托管服务清单 中的其他服务并列运行。

前置条件和实际限制

您需要一台至少有 1 GB RAM 的 VPS;当工作流开始执行实际任务后,应按 2 GB 规划,因为执行过程和 Node.js 运行时会消耗内存,而 OOM killer 在运行期间终止容器,是一种非常糟糕的方式来发现内存不足。起步时使用单个 vCPU 即可。如果这台服务器还要运行更重的服务,应优先按该服务的需求配置规格:照片库通常是主要负载,而 PhotoPrism 和 Immich 的实际内存下限远高于 n8n 的需求。媒体服务器也一样:Jellyfin 服务,加上用于浏览内容的前端(例如 Halcyon,它会将媒体库重新呈现为 90 年代的音像租赁店),会在 n8n 察觉之前就占用内存和转码余量。

您需要一个域名或子域名,例如 n8n.example.com,并为其配置指向 VPS 公网 IP 的 A 记录。在申请证书前,该记录必须已经解析生效。代理必须能够访问 80 和 443 端口;n8n 自身的 5678 端口不得直接暴露到互联网。您需要安装 Docker Engine 和 Compose plugin;如果 docker compose version 返回 docker: 'compose' is not a docker command 错误,说明您使用的是旧版独立二进制文件,而 plugin 的名称是 sudo apt install docker-compose-plugin。

测试可以使用 SQLite,关键业务应使用 Postgres

n8n 的默认数据库是位于 /home/node/.n8n/database.sqlite 的 SQLite 文件。用于快速试用时,这样配置没有问题;如果不挂载卷,第一次重新创建容器时数据就会丢失,这本身也是一个需要了解的教训。迁移到 Postgres 的原因不是原始速度,而是 SQLite 只允许单个写入锁。因此,当一个实例同时运行多个工作流,或启用你最终可能需要的队列模式时,并发操作会触发 SQLITE_BUSY: database is locked。Postgres 没有这一上限,可以使用 pg_dump 可靠地备份,这也是 n8n 官方文档对需要长期依赖的服务器所采用的方案。之后再切换意味着必须手动迁移数据,因此如果这台服务器很重要,应从 Postgres 开始。

DNS 与防火墙

先配置 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 enable

不要开放 5678。compose 文件将 n8n 绑定到 127.0.0.1:5678,因此只有主机上的反向代理可以访问它;配置 ufw allow 5678 会破坏这种隔离。

Compose 文件

创建工作目录和 docker-compose.yml。这就是完整的堆栈:两个服务、一个专用网络和两个命名卷。

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 本身。带有 condition: service_healthy 的 depends_on 可避免 n8n 在启动时与 Postgres 竞争;否则 n8n 会启动、发现数据库不存在,然后退出。位于 /home/node/.n8n 的命名卷 n8n_data 保存加密密钥;使用 SQLite 时还保存数据库。这是绝不能丢失的目录。将镜像固定为确切版本,绝不要使用 latest;原因见下方的升级章节。

密钥文件

不要将密码写入 compose 文件。将密码放在旁边的 .env 文件中,Compose 会自动读取该文件。生成密码时,应确保它们确实是随机值。

printf 'POSTGRES_PASSWORD=%s\n'  "$(openssl rand -hex 24)" >  .env
printf 'N8N_ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
chmod 600 .env

N8N_ENCRYPTION_KEY 是这里最重要的字符串。它是用于加密所有已存储凭据的密钥。请显式设置该密钥,不要让 n8n 自动生成。因为自行生成的值可以记录并恢复。n8n 使用此密钥加密第一个凭据后,更改该密钥会导致所有凭据都无法解密。因此现在设置一次,以后不要再修改这一行。

决定 Webhook 是否正常工作的环境变量

有 4 个变量决定 n8n 如何向外部说明自身信息。配置错误是 n8n 支持请求中最常见的问题。

  • N8N_HOST 是公网主机名,n8n.example.com。在代理后运行时,如果保留默认值 localhost,编辑器会尝试在您的浏览器中从 localhost 加载自身的 API,但该请求会失败。
  • N8N_PROTOCOL=https 告诉 n8n 当前通过 TLS 提供服务,因此 n8n 会将会话 Cookie 标记为 Secure,并生成 https:// URL。
  • N8N_PORT=5678 是 n8n 在容器内部监听的端口。它不是公网端口;443 由代理负责。
  • WEBHOOK_URL=https://n8n.example.com/ 最容易导致问题。n8n 根据这些值生成 Webhook 地址,供您粘贴到 Stripe、GitHub 或其他外部调用方中。如果该变量未设置或配置错误,n8n 会回退到 N8N_HOST:N8N_PORT,并生成 https://n8n.example.com:5678/webhook/...,或者更糟糕的 http://localhost:5678/webhook/...。这些地址不会报错,格式看起来也合理,但无法从互联网访问,因此调用方的请求会静默丢失。将其设置为带结尾斜杠的准确公网基础 URL,然后确认 Webhook 节点显示的 URL 不包含端口。

N8N_PROXY_HOPS=1 告诉 n8n 的 Express 服务器信任前面的一个代理。这样,限流以及读取客户端 IP 的功能就能获取真实地址,而不是代理的地址。这里有一个变量应当明确不要设置,即 N8N_RUNNERS_ENABLED。Task runner 会在独立于主进程的进程中执行 n8n Code 节点逻辑。在 1.x 版本系列中,该功能默认选择加入,变量默认值为 false,您必须将其设置为 true。n8n 2.0 默认启用 Task runner,因此本指南在 2.x 版本系列中固定该变量已弃用,n8n 文档也说明您不再需要设置它。

首次启动

docker compose up -d
docker compose ps
docker compose logs -f n8n

正常的首次启动应以 Editor is now accessible via: 行结束,其上方应有一行 n8n ready on ..., port 5678。docker compose ps 应显示两个容器 Up,并将 postgres 标记为 (healthy)。如果 n8n 一直处于 Restarting 循环中,请查看日志;问题几乎总是下文所述的数据库连接或卷权限。

使用反向代理处理 TLS

n8n 本身通过 5678 端口提供普通 HTTP;HTTPS 由前置组件终止。可选择以下两种方案。

如果您已经运行多个容器,可以将 n8n 放在自动签发 TLS 证书的 Traefik 反向代理之后,只需添加几个标签,Traefik 会自动申请并续期证书。

如果这台服务器上只有 n8n,一个配置了 Let's Encrypt 证书的 nginx 虚拟主机更简单。使用适用于 Ubuntu 24.04 的 Certbot 和 nginx TLS 配置获取证书,然后使用以下 server block:

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 向编辑器推送实时执行更新。如果缺少这两行,登录页面会加载,但随后会因连接丢失而卡住。proxy_read_timeout 3600 可避免长时间运行的执行任务被 nginx 默认的 60 秒超时中断。X-Forwarded-Proto $scheme 请求头与 N8N_PROXY_HOPS=1 配合使用:它告诉 n8n,原始请求使用的是 HTTPS,尽管代理通过普通 HTTP 连接 n8n。这样 n8n 不会将连接判断为不安全,并拒绝自己的 cookie。

第一个工作流:让它真正运行

打开 https://n8n.example.com/,创建所有者账户(见下一节),然后构建能验证整个链路的最小工作流:接收 webhook、发起 HTTP 调用、返回响应。

  1. 添加一个 Webhook 节点。将方法设置为 POST,路径设置为类似 hello 的值。该节点会显示两个 URL:Test URL 和 Production URL。许多“我的 webhook 不工作”问题都源于此。Test URL 只响应一次调用,而且仅在您点击 Listen for test event 后有效;随后就会过期。发布工作流时会注册 Production URL,此后它会持续响应请求。
  2. 在其后添加一个 HTTP Request 节点,指向任意公共 JSON API。对 https://api.github.com/zen 发起 GET 请求会返回一行字符串,这已经足够。
  3. 添加一个 Respond to Webhook 节点,并将 Webhook 节点的 Respond 选项设置为 “Using Respond to Webhook node”,这样调用方就能收到 HTTP 节点的输出。
  4. 点击右上角的 Publish,然后调用它:curl -X POST https://n8n.example.com/webhook/hello。您应收到返回的 zen 文本。请求以 POST 进入,调用 API,再返回响应,这就是大多数实际自动化流程的基本结构。较早的教程会让您在这里切换 Active 开关;n8n 2.0 已使用 Publish 和 Unpublish 替代该开关。

定时版本可将 Webhook 节点替换为 Schedule Trigger,改为调用模型端点。使用 在同一 VPS 上运行的 Ollama 这一自托管方案,可以方便地构建每晚运行的摘要程序。

用户管理,而不是基本身份验证

较早的 n8n 指南会要求设置 N8N_BASIC_AUTH_ACTIVE=true。这些变量已在 n8n 1.0 中移除,现在不会产生任何作用。当前的身份验证机制是所有者账户:首次加载编辑器时,n8n 会要求您创建一个使用电子邮件和密码的所有者账户,且此验证门槛是强制的,不存在匿名模式。首次启动后应立即创建该账户,然后再将 URL 提供给其他人:在 docker compose up 与首次提交表单之间,任何能够访问该实例的人都可以先行认领它。在反向代理上增加一层基本身份验证作为额外锁定措施是合理的,但它只是第二道防线,不是真正的身份验证。本指南中的所有者账户及其他功能都可在免费的社区版中使用;如果您之后需要支持细粒度角色的额外用户或 SSO,建议先阅读哪些 n8n 功能需要付费许可,再据此规划。

备份:先备份加密密钥,再备份数据库

需要备份两项内容,但它们的可替代性并不相同。

N8N_ENCRYPTION_KEY。 n8n 中存储的每个凭据、API 令牌、数据库密码和 OAuth 密钥,都会使用此密钥进行静态加密。如果没有该密钥,Postgres 中的工作流没有任何作用:将数据库恢复到使用其他密钥的新服务器后,n8n 无法解密任何凭据,也无法恢复或重置。您的 .env 文件中保存着该密钥;创建密钥当天就应将此文件复制到服务器之外,最好保存为密码管理器中的条目。这才是最重要的备份。

Postgres 数据库,其中包含工作流、执行历史记录和已加密的凭据:

docker compose exec -T postgres pg_dump -U n8n -d n8n \
  | gzip > n8n-db-$(date +%F).sql.gz

按计划运行该命令,并将转储文件复制到服务器之外。要在全新的 VPS 上恢复:先启动一次整个堆栈,使数据库完成初始化;停止 n8n;使用 psql 导入转储文件;将同一个 N8N_ENCRYPTION_KEY 放入 .env;然后启动 n8n。相同的密钥加上转储文件即可恢复可用实例;新密钥只会得到无法使用任何凭据的工作流。

升级:固定镜像标签

Compose 文件有意固定使用 n8nio/n8n:2.29.10,而不是 latest。n8n 大多数星期都会发布新的次要版本,不同版本之间偶尔会更改数据库架构或节点行为,因此使用 latest 意味着无人值守的拉取操作可能会获取一个在启动时立即迁移数据库的构建版本。固定版本,升级前阅读发行说明。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 文件强制执行严格权限检查,默认启用 task runners,并将工作流的 Activate 开关替换为 Publish。跨越主版本边界前,请阅读 2.0 不兼容变更页面。n8n 会在启动时自动执行所需的数据库迁移,这正是升级前执行 pg_dump 不可省略的原因。由于凭据使用 .env 中的密钥加密,数据存储在 Postgres 中,因此容器可以直接替换:固定使用之前的标签即可回滚,并从转储恢复数据。

故障模式及其显示的日志字符串

The requested webhook "POST hello" is not registered. 调用未发布工作流的 webhook 时,或在无人监听时调用测试路径时,会返回 404。只有点击“Listen for test event”后,测试路径(/webhook-test/...)才会响应;只有发布工作流后,生产路径(/webhook/...)才会响应。同级的 This webhook is not registered for GET requests. Did you mean to make a POST request? 表示方法错误:节点要求使用 POST,但您发送了 GET。

webhook URL 显示 :5678 或 localhost。 节点显示 https://n8n.example.com:5678/webhook/... 或 http://localhost:5678/...。WEBHOOK_URL 未设置或设置错误,因此 n8n 根据 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。先启动一次堆栈,然后在 .env 中修改 POSTGRES_PASSWORD,但现有的 postgres_data 卷仍保存旧密码。将其改回原密码;或者,如果没有需要保留的数据,则 docker compose down 并 docker volume rm Postgres 卷,然后重新启动。

启动时显示 EACCES: permission denied, open '/home/node/.n8n/config'。 n8n 以 node 用户(UID 1000)运行,无法写入其配置目录。如果将主机目录绑定挂载到容器中(./n8n_data:/home/node/.n8n),且该目录归 root 所有,就会出现此问题。使用上文所示的命名卷;如果必须使用绑定挂载,则先执行 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,并在启动时自动修正权限。此日志行表示它已经修正了权限,通常发生在使用绑定挂载,或恢复操作将权限过于宽松的文件复制回来之后。无需采取任何操作;只有在文件系统确实不支持权限控制时,才设置 N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false。

Mismatching encryption keys,完整日志行表示设置文件中的加密密钥 /home/node/.n8n/config 与环境中的 N8N_ENCRYPTION_KEY 不匹配。环境中的密钥不同于 n8n 上次运行时写入其数据卷的密钥。最常见的原因是:之前启动时未设置该变量,n8n 自动生成了随机密钥,之后您又设置了另一个密钥。将原始密钥放回 .env;只有在确实没有需要保留的已存储凭据时,才删除 n8n_data 卷中的 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,但通过普通 HTTP 访问了 n8n,通常是直接访问 IP 和端口,而不是通过 HTTPS 代理访问。请通过 https://n8n.example.com/ 访问。只有在确实无法使用 HTTPS 时,才设置 N8N_SECURE_COOKIE=false;面向互联网的服务器绝不能这样设置。

如需将语言模型加入这些工作流,请参阅 使用 Claude 和 n8n 构建 AI 工作流。

FAQ

我应该为 n8n 使用 SQLite 还是 Postgres?

SQLite(默认数据库)适合试用 n8n,也适合一次只运行一个工作流的个人实例。对于任何依赖 n8n 的环境,都应改用 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 不包含端口。第二个原因是调用了尚未发布的工作流的生产 URL,这会返回 The requested webhook ... is not registered.

n8n 必须备份哪些内容?

有两项。第一项是 .env 文件中的 N8N_ENCRYPTION_KEY,因为所有已存储的凭据都使用它加密。丢失该密钥后,凭据将永久无法解密。创建密钥当天就应将其复制到服务器之外。第二项是包含工作流、历史记录和凭据的 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。先执行 pg_dump,因为 n8n 会在启动时自动运行迁移;阅读发行说明,了解不兼容变更,然后更新标签并运行 docker compose pull n8n && docker compose up -d n8n。容器可以直接替换,因此回滚时只需固定到之前的标签,并恢复升级前的数据库转储。