用 Caddy 代理 Strapi
🌐 Proxying Strapi with Caddy
Page summary:将
server.url指向你的公共域,并将server.proxy.koa设置为true,这样 Strapi 就会信任转发的头信息。然后写一个三行的 Caddyfile,它会为你获取和续订 TLS 证书。
Strapi 监听的是普通的 HTTP 端口,它本身不会终止 TLS。你可以在它前面放一个反向代理,比如 Caddy,来处理 HTTPS,把你的应用提供在 443 端口,并将请求转发给 Strapi 进程。Caddy 就是通过为你申请和续期 TLS 证书来完成这个工作的。这个指南会讲如何配置 Strapi,让你的应用能识别代理,然后是如何写 Caddyfile 来路由流量到 Strapi。Strapi 的改动需要在你的项目里进行,所以部署前先改好。Caddy 的改动则是在运行 Caddy 的机器或容器上进行。
🌐 Strapi listens on a plain HTTP port and does not terminate TLS itself. A reverse proxy such as Caddy sits in front of it to handle HTTPS, serve your application on port 443, and forward requests to the Strapi process. Caddy fills that role by provisioning and renewing TLS certificates for you. This guide covers the Strapi configuration that makes your application proxy-aware, then the Caddyfile that routes traffic to it. The Strapi changes belong in your project, so make them before you deploy. The Caddy changes are made on the machine or in the container that runs Caddy.
- 一个可以在本地启动和运行的 Strapi 5 应用(参见部署指南)。
- Caddy 可以运行在与 Strapi 相同的主机上,也可以作为容器运行在同一个 Docker 网络上(参见 Caddy installation documentation)。
- 一个 DNS
A记录指向该服务器的域名。 - 端口 80 和 443 对公网开放。Caddy 需要端口 80 来完成证书挑战。
- 具有
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:
- JavaScript
- TypeScript
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'),
},
});
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'),
},
});
更改 /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
Caddy 会添加一个 X-Forwarded-For 头,包含原始客户端的 IP 地址。Strapi 会忽略这个头,直到你开启代理支持。
🌐 Caddy 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:
- JavaScript
- TypeScript
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'),
},
});
export default ({ 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.maxIpsCount | 5.52.0+ 要从转发头链的末尾读取的地址数量。对于单个代理设置为 1,如果请求经过多个代理,则设置为代理的数量。 |
proxy.ipHeader | 5.52.0+ 客户端 IP 要读取的头。默认是 X-Forwarded-For,所以只有在你的代理发送其他头(比如 CF-Connecting-IP)时才需要设置。 |
如果在没有 proxy.maxIpsCount 的情况下将 proxy.koa 设置为 true,计数会保持在默认的 0,也就是无限。将 maxIpsCount 设置为你实际在 Strapi 前面的代理数量,这样只有你的基础设施添加的地址会被读取。Caddy 默认会丢弃客户端提供的 X-Forwarded-* 值,所以当代理或 CDN 位于 Caddy 前面时,这一点就显得尤其重要。
🌐 Setting proxy.koa to true without proxy.maxIpsCount leaves the count at its default of 0, which means unlimited. Set maxIpsCount to the real number of proxies in front of Strapi so that only addresses added by your own infrastructure are read. Caddy discards client-supplied X-Forwarded-* values by default, so this matters most when a proxy or CDN sits in front of Caddy.
Strapi 会读取名为 proxy.ipHeader 的头部,默认是 X-Forwarded-For。Caddy 会设置同样的头部,所以你不需要更改它。
🌐 Strapi reads the header named by proxy.ipHeader, which defaults to X-Forwarded-For. Caddy sets that same header, so you do not need to change it.
Caddy 在这里的作用是并不是每个代理都能做到的。它会丢弃客户端发送的任何 X-Forwarded-* 值,并写入自己的值,这样客户端就无法在链中注入伪造的地址。这个保护依赖于 Caddy 是第一个看到请求的代理。如果在 Caddy 前有 CDN 或负载均衡器,应该将它声明为可信代理,这样 Caddy 就会保留上游添加的地址:
🌐 Caddy helps here in a way that not every proxy does. It discards any X-Forwarded-* values a client sends and writes its own, so a client cannot inject a fake address into the chain. That protection depends on Caddy being the first proxy to see the request. If a CDN or load balancer sits in front of Caddy, declare it as a trusted proxy so Caddy preserves the addresses that upstream added:
{
servers {
trusted_proxies static private_ranges
}
}
api.example.com {
reverse_proxy 127.0.0.1:1337
}
在 Strapi 端设置 proxy.maxIpsCount 时,计算那条链中的每一个代理。
🌐 Count every proxy in that chain when you set proxy.maxIpsCount on the Strapi side.
提高上传的文件大小限制
🌐 Raise the body size limits for uploads
Caddy 默认不会限制请求体,所以适用的是 Strapi 的限制。如果你通过媒体库上传文件,需要提高它们的大小。
🌐 Caddy does not cap request bodies 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:
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.
配置 Caddy
🌐 Configure Caddy
在 Strapi 识别了代理之后,下一步就是配置 Caddyfile 将流量转发给它。
🌐 With Strapi aware of the proxy, the next step is the Caddyfile that forwards traffic to it.
写 Caddyfile
🌐 Write the Caddyfile
Caddy 从单个文件读取其配置,默认路径为 /etc/caddy/Caddyfile。一个可用的 Strapi 代理只需三行:
🌐 Caddy reads its configuration from a single file, by default at /etc/caddy/Caddyfile. A working Strapi proxy takes 3 lines:
api.example.com {
reverse_proxy 127.0.0.1:1337
}
在区块顶部命名一个公共域会触发自动 HTTPS。Caddy 在第一次启动时会为 api.example.com 获取 Let's Encrypt 证书,将 HTTP 重定向 到 HTTPS,并在证书过期前自动续期。配置中不会出现证书路径。
🌐 Naming a public domain at the top of the block is what triggers automatic HTTPS. Caddy obtains a Let's Encrypt certificate for api.example.com on first start, redirects HTTP to HTTPS, and renews the certificate before it expires. No certificate paths appear in the configuration.
reverse_proxy 指令会自行设置 X-Forwarded-For、X-Forwarded-Proto 和 X-Forwarded-Host,这就是为什么这个块保持简短。上述 Strapi proxy 选项依赖的就是这些头部。WebSocket 连接也会在无需额外配置的情况下被代理,远程数据传输 功能就是依赖这个的。
🌐 The reverse_proxy directive sets X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host on its own, which is why the block stays short. Those are the headers the Strapi proxy options above rely on. WebSocket connections are also proxied without extra configuration, which the remote data transfer feature relies on.
Caddyfile 是大多数部署使用的格式,但它不是唯一的一种。Caddy 的原生配置格式是 JSON,配置适配器可以将 YAML、TOML、HCL、CUE,甚至现有的 Nginx 配置转换为 JSON。使用 --adapter 标志选择一种格式,就像 caddy run --config caddy.yaml --adapter yaml 中一样。完整列表请参见 Caddy config adapters documentation 。不管你选择哪种格式,本指南中的 Strapi 选项都是一样的。
重新加载 Caddy 以应用文件:
🌐 Reload Caddy to apply the file:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
caddy validate 命令在你重新加载之前会检查配置。重新加载有问题的配置会让网站宕机,所以别跳过这一步。
🌐 The caddy validate command checks the configuration before you reload. Reloading a broken configuration takes the site down, so do not skip it.
设置上传大小上限
🌐 Set an upload size cap
除非你另有说明,否则 Caddy 可以接受任何大小的请求体。在 /etc/caddy/Caddyfile 中设置明确限制会在代理阶段就拒绝超大上传,而不是等 Strapi 缓存后才处理:
🌐 Caddy accepts request bodies of any size unless you tell it otherwise. Setting an explicit cap in /etc/caddy/Caddyfile rejects oversized uploads at the proxy rather than after Strapi has buffered them:
api.example.com {
request_body {
max_size 100MB
}
reverse_proxy 127.0.0.1:1337
}
在 Strapi 的 body 中将该值保持在 formidable.maxFileSize 或以上,否则 Caddy 会拒绝 Strapi 本来可以接受的上传。不要拿 formLimit 来比较:它限制的是普通表单字段,而不是上传的文件。
🌐 Keep this value at or above formidable.maxFileSize in the Strapi body middleware, otherwise Caddy rejects uploads that Strapi would have accepted. formLimit is not the one to compare against: it caps ordinary form fields, not the uploaded file.
max_size 子指令从 Caddy v2.3.0 开始可用。在更早的版本中,可以省略这个块,让 Strapi 中的 formidable.maxFileSize 来限制即可。
🌐 The max_size subdirective is available since Caddy v2.3.0. On an earlier version, leave the block out and let formidable.maxFileSize in Strapi enforce the limit instead.
代理到运行在容器中的 Strapi
🌐 Proxy to Strapi running in a container
当 Caddy 和 Strapi 都以容器运行时,reverse_proxy 通过名称定位 Strapi 服务。在 Caddy 容器内部,127.0.0.1 指的是该容器本身,而不是 Strapi。下面的 Caddyfile 由接下来的 Compose 文件挂载到容器中:
🌐 When Caddy and Strapi both run as containers, reverse_proxy targets the Strapi service by name. Inside the Caddy container, 127.0.0.1 refers to that container itself, not to Strapi. The Caddyfile below is mounted into the container by the Compose file that follows:
api.example.com {
request_body {
max_size 100MB
}
reverse_proxy strapi:1337
}
Strapi 还必须绑定到 0.0.0.0。绑定到 localhost 时,它只接受来自自己容器内的连接,Caddy 无法访问它。本指南前面显示的 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 Caddy cannot reach it. The host value shown earlier in this guide already uses 0.0.0.0.
下面的 Compose 文件把两个服务放在同一个网络上,并且只发布 Caddy:
🌐 The following Compose file puts both services on one network and publishes only Caddy:
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
caddy:
image: caddy:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
depends_on:
- strapi
networks:
- web
volumes:
caddy_data:
networks:
web:
在 /data 挂载一个持久卷,如上所示。Caddy 会把发放的证书存放在那里。如果没有,每次容器重启都会请求新的证书,这会达到 Let's Encrypt rate limits ,并且在限制重置之前网站没有有效证书。
检查代理设置
🌐 Verify the proxy setup
Strapi 在 /_health 提供了一个健康检查路由,该路由会返回 HTTP 204 No Content 和一个 strapi 头。通过代理请求它可以确认 Caddy 已经连接到 Strapi:
🌐 Strapi exposes a health check route at /_health that responds with HTTP 204 No Content and a strapi header. Requesting it through the proxy confirms that Caddy reaches Strapi:
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:
- 在浏览器中打开
https://api.example.com/admin并登录。管理面板通过 HTTPS 加载,没有证书警告。 - 在媒体库上传一张图片。它的 URL 使用你的域名,而不是
localhost:1337。 - 检查 Strapi 输出中的真实客户端 IP 地址,而不是
127.0.0.1。如果 Strapi 在前台运行,地址会出现在那个终端里。
故障排除
以下每一个症状都指向设置的一侧。症状以粗体显示,后面跟着原因以及需要更改的地方:
🌐 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:
- Caddy 获取证书失败。 证书验证需要公网上可以访问端口 80,并且该域名的 DNS
A记录必须已经指向这台服务器。先检查这两点,然后用journalctl -u caddy --no-pager | tail -50查看 Caddy 日志。 - Caddy 返回
502 Bad Gateway。 Caddy 无法连接到 Strapi。请确认 Strapi 进程正在运行并监听reverse_proxy中使用的端口,然后检查/config/server.js中的host没有绑定到 Caddy 无法访问的接口。 - 上传被拒绝,因为文件太大。 请求超过了大小限制。在 Caddyfile 的
request_body块中提高max_size,在 Strapi 的body中间件中提高formidable.maxFileSize,如果文件大于 1 GB,再提高提供商sizeLimit。 - Strapi 把
127.0.0.1记录为客户端 IP。proxy.koa没有设置为true,所以 Strapi 读取的是套接字地址,而不是转发的头信息。 - 密码重置邮件链接到
localhost:1337。url选项未设置或仍指向本地地址。将其设置为公共 URL 然后重建管理面板。
下一步
🌐 Next steps