使用 Traefik 代理 Strapi
页面摘要: 使用
server.url和server.proxy选项告知 Strapi 它的公开地址以及前方存在代理。然后为 Strapi 容器添加 Traefik 标签,使 Traefik 能够发现它、将流量路由到 1337 端口,并为你的域名申请证书。
Strapi 监听一个普通的 HTTP 端口,自身并不终止 TLS。诸如 Traefik 这样的反向代理位于其前方,用于处理 HTTPS、在 443 端口提供你的应用,并将请求转发给 Strapi 进程。Traefik 与本节中的其他代理有所不同:它不通过描述路由的配置文件来工作,而是监听 Docker API,并从每个容器上的标签读取路由规则。这适合容器不断创建和销毁的部署场景。本指南先介绍让你的应用感知代理的 Strapi 配置,然后是将来流量路由到它的 Traefik 设置。
- 一个能够在本地启动并运行的 Strapi 5 应用(参见 部署指南)。
- Strapi 已被打包为容器镜像(参见 Docker 安装)。
- 主机上已安装 Docker Compose。
- 一个 DNS
A记录指向该主机的域名。 - 80 和 443 端口对公网开放。Traefik 需要其中至少一个端口来完成证书挑战。
为反向代理配置 Strapi
Strapi 需要知道它对外提供服务的公开地址,并且需要信任代理添加的头部。如果没有这两项设置,Strapi 会从 localhost:1337 构建 URL,并将代理的 IP 地址读取为客户端 IP。
设置公开 URL
服务器配置中的 url 选项定义了应用的公共地址。Strapi 使用它构建密码重置邮件、第三方登录提供方和媒体资源路径的绝对 URL。
将其设置为你应用访问者在浏览器中使用的地址:
JavaScript
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'),
},
});
TypeScript
export default ({ 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'),
},
});
host 值必须保持为 0.0.0.0。如果绑定到 localhost,Strapi 只接受来自自身容器内部的连接,Traefik 将无法访问它。
更改 /config/server.js 需要重新构建管理面板。保存文件后请运行 yarn build 或 npm run build,然后重新构建你的镜像。
信任代理头部
Traefik 会添加一个携带原始客户端 IP 地址的 X-Forwarded-For 头部。在你开启代理支持之前,Strapi 会忽略该头部。
通过服务器配置中的 proxy 选项启用代理支持:
JavaScript
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://api.example.com'),
// highlight-start
proxy: {
koa: true,
maxIpsCount: 1,
},
// highlight-end
app: {
keys: env.array('APP_KEYS'),
},
});
TypeScript
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://api.example.com'),
// highlight-start
proxy: {
koa: true,
maxIpsCount: 1,
},
// highlight-end
app: {
keys: env.array('APP_KEYS'),
},
});
每个选项起不同作用:
| 选项 | 作用 |
|---|---|
proxy.koa | 当为 true 时,Strapi 信任 X-Forwarded-* 请求头。客户端 IP、协议和主机从代理读取,而非 socket。 |
proxy.maxIpsCount | (v5.52.0+)从转发请求头链末尾读取的地址数量。单个代理设为 1,若请求经过多个代理则设为代理数量。 |
proxy.ipHeader | (v5.52.0+)用于读取客户端 IP 的请求头。默认为 X-Forwarded-For,仅在代理发送其他请求头(如 CF-Connecting-IP)时设置。 |
将 proxy.koa 设为 true 但未设置 proxy.maxIpsCount 时,计数会保留其默认值 0,即无限制。请将 maxIpsCount 设为 Strapi 前方代理的真实数量,以便只读取由你自己的基础设施添加的地址。
Strapi 读取由 proxy.ipHeader 指定的头部,其默认值为 X-Forwarded-For,Traefik 设置的正是这个头部。Traefik 还会设置 X-Forwarded-Proto,Strapi 使用它来将管理面板的刷新令牌 cookie 标记为 Secure。密码重置、登录提供方回调以及媒体资源的绝对 URL 是基于 url 选项构建的,而非基于该头部。
Traefik 在此处提供的保护并非每个代理都具备。默认情况下,它会清理客户端发送的 X-Forwarded-* 头部并写入自己的值,因此客户端无法向链条中注入伪造地址。该保护依赖于 Traefik 是第一个看到请求的代理。如果 Traefik 前方还有 CDN 或负载均衡器,请在入口点的 forwardedHeaders.trustedIPs 中列出其地址,使 Traefik 保留上游添加的内容,然后在设置 proxy.maxIpsCount 时将链条中的每个代理都计算在内:
entryPoints:
websecure:
address: ':443'
forwardedHeaders:
trustedIPs:
- '203.0.113.0/24'
提高上传的体积上限
Traefik 默认会流式将请求体转发给后端,且不设大小上限,除非你主动添加。因此默认适用的是 Strapi 的限制。如果你通过媒体库上传文件,请提高这些限制。
在 Strapi 侧,body 中间件解析传入的请求。上传的文件以 multipart 数据到达,因此 formidable.maxFileSize 是限制它们的选项。formLimit 和 jsonLimit 选项覆盖普通表单字段和 JSON 负载,而非文件本身:
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。要更改它,请参阅本地上传提供方配置和最大文件大小。
配置 Traefik
Traefik 读取两类配置。静态配置定义入口点和证书解析器,在 Traefik 启动时一次性设置。动态配置定义路由,来自容器标签,并随容器的出现而被拾取。
编写静态配置
以下文件定义了每个公开端口上的入口点,将 HTTP 重定向到 HTTPS,并注册了一个 Let's Encrypt 证书解析器:
entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ':443'
providers:
docker:
# 仅路由显式启用 traefik.enable=true 的容器
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /acme/acme.json
tlsChallenge: {}
将 exposedByDefault 设为 false 很重要。若保留默认值,Traefik 会发布它能看到的每一个容器,包括你本不打算暴露的那些。
通过标签将流量路由到 Strapi
静态配置就位后,Strapi 容器通过标签声明自己的路由:
services:
traefik:
image: traefik:v3.7
ports:
- '80:80'
- '443:443'
volumes:
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_acme:/acme
networks:
- web
strapi:
image: my-strapi-app
environment:
HOST: 0.0.0.0
PORT: 1337
PUBLIC_URL: https://api.example.com
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.strapi.rule=Host(`api.example.com`)'
- 'traefik.http.routers.strapi.entrypoints=websecure'
- 'traefik.http.routers.strapi.tls.certresolver=letsencrypt'
- 'traefik.http.services.strapi.loadbalancer.server.port=1337'
networks:
- web
volumes:
traefik_acme:
networks:
web:
loadbalancer.server.port 标签是容易被遗漏的一项。Traefik 路由到容器网络内部的一个端口,因此 Strapi 容器自身无需 ports 端口映射。只有 Traefik 向主机发布端口,从而使 Strapi 不暴露到公共接口。
此文件中有两个部分的影响超出了 Traefik 本身:
-
为证书存储路径挂载持久卷,如上所示使用
traefik_acme。Traefik 将签发的证书存储在acme.json中。如果没有它,每次容器重启都会请求新证书,从而触及 Let's Encrypt 速率限制,导致站点在限制重置之前没有有效证书。 -
挂载 Docker socket 会赋予 Traefik 对 Docker API 的完全访问权限,这等同于主机上的 root。上面所示的
:ro标志作用于 socket 文件而非 API,因此它并不能限制 Traefik 通过 socket 所能执行的操作。应将其视为一种约定,而非边界。请勿将 Traefik 仪表盘公开暴露,对于加固后的部署,请将 socket 置于一个仅暴露 Traefik 所需端点的代理之后。
限制请求体大小
默认情况下 Traefik 不施加请求大小限制。为了在代理层而非在 Strapi 处拒绝过大的上传,请向 Strapi 容器的标签添加 buffering 中间件:
labels:
# ...
- 'traefik.http.middlewares.strapi-limit.buffering.maxRequestBodyBytes=104857600'
- 'traefik.http.routers.strapi.middlewares=strapi-limit'
超过限制的请求会收到 413 响应。
buffering 中间件会在转发之前读取整个请求体,超过阈值时会溢出到磁盘。这会给每次上传增加延迟和磁盘占用,对于接受大文件的媒体库来说是一笔不划算的代价。如果你的上传文件较大,请保留该中间件关闭,改由 Strapi 中的 formidable.maxFileSize 强制执行限制。
验证代理设置
启动整个栈并检查 Traefik 能否访问 Strapi。Strapi 在 /_health 暴露了一个健康检查路由,以 HTTP 204 No Content 和一个 strapi 头部作为响应:
docker compose up -d
curl -I https://api.example.com/_health
正常的设置会返回状态行和头部:
HTTP/2 204
strapi: You are so French!
然后确认链条的其余部分:
- 在浏览器中打开
https://api.example.com/admin并登录。管理面板通过 HTTPS 加载,且拥有有效证书。 - 在媒体库中上传一张图片。其 URL 使用的是你的域名,而非
localhost:1337。 - 使用
docker compose logs strapi检查 Strapi 容器日志,确认显示的是真实的客户端 IP 地址,而非 Traefik 容器地址。
故障排查
以下每个症状都指向设置中的某一方面。症状以粗体显示,其后为导致该症状的原因以及需要更改的内容:
-
Traefik 返回
404 page not found。 没有路由器匹配该请求。请确认容器带有traefik.enable=true,Host()规则与你请求的域名相匹配,并且两个容器共享同一个 Docker 网络。 -
Traefik 返回
502 Bad Gateway。 Traefik 匹配到了路由但无法访问 Strapi。请确认loadbalancer.server.port为1337,且 Strapi 绑定到0.0.0.0而非localhost。 -
没有签发证书。 对于
tlsChallenge,证书挑战需要 443 端口可从公网访问,并且域名的 DNSA记录必须已经解析到该主机。请使用docker compose logs traefik检查 Traefik 日志。 -
证书在每次重启时都被重新签发。 证书存储路径不在持久卷上,因此
acme.json随容器一起丢失。 -
上传失败并返回
413响应。buffering中间件的限制低于文件大小。请提高maxRequestBodyBytes,或移除该中间件,改由 Strapi 中的formidable.maxFileSize强制执行限制。 -
Strapi 将 Traefik 容器地址记录为客户端 IP。
proxy.koa未设为true,因此 Strapi 读取的是 socket 地址,而非转发的头部。 -
密码重置邮件链接到
localhost:1337。url选项未设置,或仍指向本地地址。请将其设为公开 URL,重新构建管理面板,并重新构建镜像。