n8n VPS 自托管:Docker、Postgres 与 HTTPS 配置
在 VPS 上用 Docker Compose 部署 n8n、Postgres 和 HTTPS 反向代理,详解 WEBHOOK_URL、加密密钥、5678 端口暴露及常见错误字符串。
构建内容
n8n 是一个工作流自动化工具:它提供可视化编辑器。当触发器、webhook、计划任务或表单提交发生时,编辑器会触发一系列节点。这些节点可以调用 API、转换数据,并将数据写入其他系统。由于无需自行编写服务即可连接各种模型提供商和数据库,n8n 已成为 AI agent 工作流的默认集成工具。使用一个 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 .envN8N_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:任务运行器会在单独的沙箱进程中运行 n8n 的 Code 节点逻辑。从 1.69 起,任务运行器已是默认设置;本指南固定使用的 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 请求、返回响应。
- 添加一个 Webhook 节点。将方法设置为
POST,路径设置为类似hello的值。它会显示两个 URL:Test URL 和 Production URL。许多“我的 webhook 无法工作”的问题都源于此。Test URL 只会响应一次请求,而且仅在您点击 Listen for test event 后有效;随后它就会过期。只要工作流处于 Active 状态,Production URL 就会响应请求。 - 在它后面添加一个 HTTP Request 节点,并将其指向任意公开的 JSON API。对
https://api.github.com/zen发起 GET 请求会返回一行字符串,这已经足够。 - 添加一个 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 这类自托管模型,可以方便地构建每夜运行的摘要生成器。
用户管理,而不是基本身份验证
较早的 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 文件强制实施严格权限控制。跨越主版本边界前,请阅读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? 表示请求方法错误:节点要求 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 不包含端口。第二个原因是调用了未切换为 Active 的工作流中的 Webhook,这会返回 The requested webhook ... is not registered.
n8n 必须备份哪些内容?
两项内容。第一项是 .env 文件中的 N8N_ENCRYPTION_KEY,因为每个已存储的凭据都使用它加密。丢失该密钥后,凭据将永久无法解密。创建密钥当天就应将其复制到服务器之外。第二项是 Postgres 数据库的 pg_dump,其中包含工作流、历史记录和凭据。恢复时两者都需要:相同的密钥和数据库转储。
如何通过 HTTPS 暴露 n8n?
n8n 在 5678 端口提供纯 HTTP;前置反向代理负责终止 TLS。将 n8n 绑定到 127.0.0.1:5678,使其只能由代理访问,然后使用 Traefik 自动申请证书,或使用 nginx 配置 Let's Encrypt 证书。设置 N8N_PROTOCOL=https 和 WEBHOOK_URL=https://your-host/,并确认代理转发 WebSocket 的 Upgrade 请求头,否则编辑器会卡住。
如何安全升级 n8n?
固定具体的镜像标签,不要使用 latest。先执行 pg_dump,因为 n8n 会在启动时自动运行迁移。阅读发行说明,确认是否存在不兼容变更,然后更新标签并运行 docker compose pull n8n && docker compose up -d n8n。容器可以直接替换,因此回滚时只需固定到之前的标签,并恢复升级前的数据库转储。