Skip to main content

用 HAProxy 代理 Strapi

🌐 Proxying Strapi with HAProxy

Page summary:

使用 server.url 和 server.proxy 选项告诉 Strapi 它的公共地址以及前面有一个代理。然后定义一个终止 TLS 的 HAProxy 前端,以及一个在 /_health 上对 Strapi 进行健康检查的后端。

Strapi 监听一个普通的 HTTP 端口,本身不会处理 TLS。你可以在它前面放一个反向代理,比如 HAProxy 来处理 HTTPS,在 443 端口提供你的应用,并将请求转发给 Strapi 进程。当你希望跨多个 Strapi 实例进行健康检查和负载均衡时,HAProxy 是一个不错的选择。本指南讲的是让你的应用支持代理的 Strapi 配置,然后是将流量路由到它的 HAProxy 配置。Strapi 的改动属于你的项目,所以在部署前先做。HAProxy 的改动是在运行 HAProxy 的机器或容器上完成的。

🌐 Strapi listens on a plain HTTP port and does not terminate TLS itself. A reverse proxy such as HAProxy sits in front of it to handle HTTPS, serve your application on port 443, and forward requests to the Strapi process. HAProxy is a good fit when you also want health checking and load balancing across more than one Strapi instance. This guide covers the Strapi configuration that makes your application proxy-aware, then the HAProxy configuration that routes traffic to it. The Strapi changes belong in your project, so make them before you deploy. The HAProxy changes are made on the machine or in the container that runs HAProxy.

Prerequisites
  • 一个可以在本地启动和运行的 Strapi 5 应用(参见部署指南)。
  • HAProxy 2.2 或更高版本,可以运行在与 Strapi 相同的主机上,或者作为一个可以访问 Strapi 的容器(参见 HAProxy installation instructions)。本指南中的健康检查语法需要 2.2 或更高版本。
  • 一个 DNS A 记录指向 HAProxy 主机的域名。
  • 一个 TLS 证书和私钥,合并成一个 PEM 文件。
  • 具有 sudo 权限的 Shell 访问。

为反向代理配置 Strapi​

🌐 Configure Strapi for a reverse proxy

Strapi 需要知道它被访问的公共地址,并且需要信任代理添加的头信息。没有这两个设置,Strapi 会用 localhost:1337 构建 URL,并把代理的 IP 地址当作客户端 IP。

🌐 Strapi needs to know the public address it is served from, and it needs to trust the headers the proxy adds. Without these 2 settings, Strapi builds URLs from localhost:1337 and reads the proxy's IP address as the client IP.

设置公共网址​

🌐 Set the public URL

服务器配置中的 url 选项定义了你应用的公网地址。Strapi 会用它来生成密码重置邮件、第三方登录提供商以及媒体资源路径的绝对 URL。

🌐 The url option in the server configuration defines the public address of your application. Strapi uses it to build absolute URLs for password reset emails, third-party login providers, and media asset paths.

将它设置为你应用访客在浏览器中使用的地址:

🌐 Set it to the address your application's visitors use in their browser:

/config/server.js
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://api.example.com'),
app: {
keys: env.array('APP_KEYS'),
},
});
Caution

更改 /config/server.js 需要重建管理面板。保存文件后运行 yarn build 或 npm run build。

🌐 Changing /config/server.js requires rebuilding the admin panel. Run yarn build or npm run build after saving the file.

信任代理头​

🌐 Trust the proxy headers

HAProxy 会添加一个 X-Forwarded-For 头,包含原始客户端的 IP 地址。Strapi 会忽略这个头,直到你开启代理支持。

🌐 HAProxy adds an X-Forwarded-For header carrying the original client IP address. Strapi ignores that header until you turn proxy support on.

通过服务器配置中的 proxy 选项启用代理支持:

🌐 Enable proxy support through the proxy options in the server configuration:

/config/server.js
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://api.example.com'),
proxy: {
koa: true,
maxIpsCount: 1,
},
app: {
keys: env.array('APP_KEYS'),
},
});

每个选项都有不同的作用:

🌐 Each option plays a different role:

选项作用
proxy.koa当 true 时,Strapi 会信任 X-Forwarded-* 头。客户端 IP、协议和主机将从代理读取,而不是从套接字读取。
proxy.maxIpsCount5.52.0+ 要从转发头链的末尾读取的地址数量。对于单个代理设置为 1,如果请求经过多个代理,则设置为代理的数量。
proxy.ipHeader5.52.0+ 客户端 IP 要读取的头。默认是 X-Forwarded-For,所以只有在你的代理发送其他头(比如 CF-Connecting-IP)时才需要设置。
IP spoofing

如果在没有设置 proxy.maxIpsCount 的情况下将 proxy.koa 设置为 true,计数会保持在默认值 0,也就是无限制。这样客户端就可以发送 X-Forwarded-For: 203.0.113.9,一旦 HAProxy 添加了真实地址,Strapi 会从链条前端读取伪造的值,而不是末端的真实地址。一定要将 maxIpsCount 设置为 Strapi 前面代理的真实数量。

🌐 Setting proxy.koa to true without proxy.maxIpsCount leaves the count at its default of 0, which means unlimited. A client can then send X-Forwarded-For: 203.0.113.9 and, once HAProxy appends the real address, Strapi reads the spoofed value from the front of the chain instead of the real one at the end. Always set maxIpsCount to the real number of proxies in front of Strapi.

Strapi 会读取名为 proxy.ipHeader 的头部,默认值是 X-Forwarded-For。下面配置中的 option forwardfor 指令设置了相同的头部,所以你不需要更改它。

🌐 Strapi reads the header named by proxy.ipHeader, which defaults to X-Forwarded-For. The option forwardfor directive in the configuration below sets that same header, so you do not need to change it.

提高上传的文件大小限制​

🌐 Raise the body size limits for uploads

HAProxy 默认不会限制请求体的大小,所以生效的限制是 Strapi 的。如果你通过媒体库上传文件,可以把限制提高。

🌐 HAProxy does not cap request body size by default, so the Strapi limits are the ones that apply. If you upload files through the Media Library, raise them.

在 Strapi 这边,body 中间件会解析传入的请求。上传的文件会以 multipart 数据的形式到来,所以 formidable.maxFileSize 是限制它们的选项。formLimit 和 jsonLimit 选项处理普通表单字段和 JSON 数据,而不是文件本身:

🌐 On the Strapi side, the body middleware parses incoming requests. Uploaded files arrive as multipart data, so formidable.maxFileSize is the option that caps them. The formLimit and jsonLimit options cover ordinary form fields and JSON payloads, not the file itself:

/config/middlewares.js
module.exports = [
// ...
{
name: 'strapi::body',
config: {
formLimit: '100mb', // form body
jsonLimit: '100mb', // JSON body
textLimit: '100mb', // text body
formidable: {
maxFileSize: 100 * 1024 * 1024, // uploaded file size, in bytes
},
},
},
// ...
];

媒体库提供者会执行一个单独的 sizeLimit,默认值是 1 GB。要更改它,请参阅 本地上传提供者配置 和 最大文件大小。

🌐 The Media Library provider enforces a separate sizeLimit, which defaults to 1 GB. To change it, see local upload provider configuration and max file size.

大型上传也需要在 HAProxy 超时设置中留出空间,下面的配置将其设置为 60 秒。如果上传完成所需时间超过这个值,请调高 timeout client 和 timeout server。

🌐 Large uploads also need room in the HAProxy timeouts, which the configuration below sets to 60 seconds. Raise timeout client and timeout server if uploads take longer than that to complete.

配置 HAProxy​

🌐 Configure HAProxy

在 Strapi 了解代理的情况下,下一步是 HAProxy 的前端和后端,将流量传输给它。

🌐 With Strapi aware of the proxy, the next step is the HAProxy frontend and backend that carry traffic to it.

写配置​

🌐 Write the configuration

HAProxy 从 /etc/haproxy/haproxy.cfg 读取它的配置。下面定义了一个接收公共流量的前端和一个将流量转发到 Strapi 的后端:

🌐 HAProxy reads its configuration from /etc/haproxy/haproxy.cfg. The following defines a frontend that accepts public traffic and a backend that forwards it to Strapi:

global
log /dev/log local0

# Needed by the `show stat` command used when verifying the setup
stats socket /var/run/haproxy.sock mode 660 level admin

defaults
mode http
log global
option httplog
option forwardfor
timeout connect 5s
timeout client 60s
timeout server 60s

frontend strapi_front
bind :80
bind :443 ssl crt /etc/haproxy/certs/api.example.com.pem

# Send every plain HTTP request to HTTPS
http-request redirect scheme https unless { ssl_fc }

# Tell Strapi the original request arrived over HTTPS
http-request set-header X-Forwarded-Proto https if { ssl_fc }

default_backend strapi_back

backend strapi_back
option httpchk
http-check send meth GET uri /_health
http-check expect status 204

server strapi1 127.0.0.1:1337 check

那个文件里的3个指令完成了Strapi依赖的工作。

option forwardfor 会添加携带客户端地址的 X-Forwarded-For 头。没有它的话,Strapi 只能看到 HAProxy 的地址。

http-request set-header X-Forwarded-Proto https if { ssl_fc } 是手动设置的,因为 HAProxy 不会为你添加它。Strapi 使用这个头来将管理员面板的刷新令牌 Cookie 标记为 Secure。密码重置、登录提供者回调和媒体资源的绝对 URL 则是从 url 选项构建的,而不是从这个头部获取。

bind :443 ssl crt 期望将证书和私钥合并到一个 PEM 文件中。这和 Nginx 不一样,Nginx 是把它们作为两个单独的路径来使用的。

验证文件并重新加载 HAProxy:

🌐 Validate the file and reload HAProxy:

terminal
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

haproxy -c 命令会检查配置,但不会应用它。重新加载损坏的配置会让网站宕机,所以不要跳过这一步。

🌐 The haproxy -c command checks the configuration without applying it. Reloading a broken configuration takes the site down, so do not skip it.

代理到运行在容器中的 Strapi​

🌐 Proxy to Strapi running in a container

当 HAProxy 和 Strapi 都作为容器运行时,server 这一行通过名字指向 Strapi 服务。在 HAProxy 容器内部,127.0.0.1 指的是容器自身,而不是 Strapi:

🌐 When HAProxy and Strapi both run as containers, the server line targets the Strapi service by name. Inside the HAProxy container, 127.0.0.1 refers to that container itself, not to Strapi:

backend strapi_back
option httpchk
http-check send meth GET uri /_health
http-check expect status 204

server strapi1 strapi:1337 check

Strapi 还必须绑定到 0.0.0.0。绑定到 localhost 时,它只接受来自自身容器内部的连接,HAProxy 无法访问它。在 设置公共 URL 下设置的 host 值已经使用了 0.0.0.0。

🌐 Strapi must also bind to 0.0.0.0. Bound to localhost, it accepts connections only from inside its own container and HAProxy cannot reach it. The host value set under Set the public URL already uses 0.0.0.0.

下面的 Compose 文件把两个服务放在同一个网络上,并且只发布 HAProxy:

🌐 The following Compose file puts both services on one network and publishes only HAProxy:

./docker-compose.yml
services:
strapi:
image: my-strapi-app
environment:
HOST: 0.0.0.0
PORT: 1337
PUBLIC_URL: https://api.example.com
# expose keeps the port reachable inside the network only
expose:
- '1337'
networks:
- web

haproxy:
image: haproxy:2.8-alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./haproxy/certs:/etc/haproxy/certs:ro
depends_on:
- strapi
networks:
- web

networks:
web:

HAProxy 在启动时只解析一次 strapi。如果 Strapi 容器被重新创建并分配了新的地址,重载 HAProxy,或者声明一个 resolvers 配置块,让它自己重新解析这个名字。

🌐 HAProxy resolves strapi once at startup. If the Strapi container is recreated with a new address, reload HAProxy, or declare a resolvers section so it re-resolves the name on its own.

健康检查 Strapi​

🌐 Health check Strapi

Strapi 提供了一个健康检查路由 /_health,会返回 HTTP 204 No Content。上面的后端使用它来决定某个实例是否应该接收流量:

🌐 Strapi exposes a health check route at /_health that responds with HTTP 204 No Content. The backend above uses it to decide whether an instance should receive traffic:

backend strapi_back
option httpchk
http-check send meth GET uri /_health
http-check expect status 204

server strapi1 127.0.0.1:1337 check

http-check send 这一行很重要。保持默认的话,option httpchk 会通过 HTTP/1.0 发送一个 OPTIONS 请求,所以明确指定方法和 URI 才能让检查把 /_health 当作 GET 来命中。然后 http-check expect status 204 这一行就要求返回的状态必须和 Strapi 一致。没有它的话,HAProxy 会接受任何 2xx 或 3xx 响应,这样会把不相关的重定向也当作健康状态。

🌐 The http-check send line matters. Left to its default, option httpchk sends an OPTIONS request over HTTP/1.0, so naming the method and URI explicitly is what makes the check hit /_health as a GET. The http-check expect status 204 line then requires exactly the status Strapi returns. Without it, HAProxy accepts any 2xx or 3xx response, which would treat an unrelated redirect as healthy.

运行几个 Strapi 实例​

🌐 Run several Strapi instances

为每个实例添加一行 server,并添加一个平衡算法来分配它们之间的流量:

🌐 Add a server line per instance and a balancing algorithm to spread traffic across them:

backend strapi_back
balance roundrobin
option httpchk
http-check send meth GET uri /_health
http-check expect status 204

server strapi1 10.0.0.11:1337 check
server strapi2 10.0.0.12:1337 check

因为这些实例共享一个数据库,在扩展之前,有一些 Strapi 的行为需要注意。

🌐 Because the instances share one database, a few Strapi behaviors need attention before you scale out.

Concurrent schema synchronization

Strapi 会在启动时运行它的模式同步,每个进程执行一次,并且实例之间没有共享锁。如果重启时内容类型没有改变是安全的,因为同步会检测到模式没变并且不会做任何操作。

🌐 Strapi runs its schema synchronization on startup, once per process, with no lock shared between instances. A restart that leaves the content-types unchanged is safe, because the synchronization detects an unchanged schema and does nothing.

在两种情况下同步是不安全的:一种是会更改内容类型的发布,另一种是有待处理迁移的发布。实例一起启动后,会对同一个数据库发起并发的架构更改和迁移。

🌐 The synchronization is not safe in 2 cases: a release that changes content-types, and a release with pending migrations. Instances starting together then issue concurrent schema changes and migrations against the same database.

对于这些版本,先启动或运行一个实例,让它完成启动后再启动下一个。这是对同一个数据库运行多个进程的特性,所以把实例分散到不同主机上也无法避免。

🌐 For those releases, start or roll a single instance first and let it finish booting before the next one starts. This is a property of running several processes against one database, so spreading the instances across separate hosts does not avoid it.

运行多个实例还有另外三个值得了解的后果:

🌐 Running several instances has 3 other consequences worth knowing:

  • CRON 任务 是在每个 Strapi 进程中调度的,所以一个任务是每个实例运行一次,而不是总共运行一次。设置为每晚运行的任务将根据你的实例数量运行多次。如果你的项目将 cron.enabled 设置为 true,要么把任务移出 Strapi,要么只在一个实例上保留它们。
  • Strapi 里的任何内存数据都只属于一个实例,因为每个实例都是一个独立的进程。Strapi 核心中没有东西会在它们之间同步状态。
  • 默认的本地上传提供商会将文件写入实例自己的磁盘。共享同一台机器和项目目录的实例会共享该磁盘,但不同机器或不同容器中的实例不会。在这种情况下,请使用由对象存储支持的媒体库提供商之一。

检查代理设置​

🌐 Verify the proxy setup

通过代理请求健康检查路径可以确认 HAProxy 是否能访问 Strapi:

🌐 Requesting the health check route through the proxy confirms that HAProxy reaches Strapi:

terminal
curl -I https://api.example.com/_health

一个可用的设置会返回状态行和头部:

🌐 A working setup returns the status line and the header:

HTTP/2 204
strapi: You are so French!

然后确认链的其余部分:

🌐 Then confirm the rest of the chain:

  1. 在浏览器中打开 https://api.example.com/admin 并登录。管理面板通过 HTTPS 加载,没有混合内容警告。
  2. 在媒体库上传一张图片。它的 URL 使用你的域名,而不是 localhost:1337。
  3. 检查 Strapi 的输出,找真正的客户端 IP 地址,而不是 HAProxy 的地址。
  4. 确认 HAProxy 认为后端是健康的。show stat 命令会报告每个服务器的状态:
terminal
echo "show stat" | sudo socat stdio /var/run/haproxy.sock

故障排除​

以下每一个症状都指向设置的一侧。症状以粗体显示,后面跟着原因以及需要更改的地方:

🌐 Each of the following symptoms points at one side of the setup. The symptom is in bold, followed by what causes it and what to change:

  • HAProxy 返回 503 Service Unavailable。 没有后端服务器通过健康检查。确认 Strapi 进程正在运行,并且从 HAProxy 主机访问 curl -i http://127.0.0.1:1337/_health 返回 204。
  • 即使 Strapi 响应,健康检查也会失败。 http-check expect status 204 行需要严格为 204。如果路由前的代理或中间件改变了状态,请调整预期值以匹配 Strapi 实际返回的内容。
  • 上传失败或超时。 在 Strapi 的 body 中间件中引发 formidable.maxFileSize,并在 HAProxy 中引发 timeout client 和 timeout server,以便传输有足够时间完成。
  • Strapi 会把 HAProxy 的地址记录为客户端 IP。 要么 HAProxy 配置里缺少 option forwardfor,要么 Strapi 里 proxy.koa 没有设置为 true。
  • 管理面板刷新 cookie 没有标记为 Secure。 X-Forwarded-Proto 头没有被设置,所以 Strapi 会把请求当作普通 HTTP 来处理。cookie 仍然会通过 HTTPS 到达浏览器,因为它默认是 SameSite=Lax,但它会失去那个让它在普通 HTTP 下无效的 Secure 标记。在前端加上 http-request set-header 这一行就行了。
  • 密码重置邮件链接到 localhost:1337。 url 选项未设置或仍指向本地地址。将其设置为公共 URL 然后重建管理面板。

下一步​

🌐 Next steps