使用 Caddy 代理 Strapi

页面摘要: 将 server.url 指向你的公开域名,并将 server.proxy.koa 设为 true,以便 Strapi 信任转发的请求头。然后编写一个 3 行的 Caddyfile,它会自动为你获取并续期 TLS 证书。

Strapi 监听一个普通的 HTTP 端口,自身并不终止 TLS。诸如 Caddy 这样的反向代理位于其前方,用于处理 HTTPS、在 443 端口提供你的应用,并将请求转发给 Strapi 进程。Caddy 通过为你配置和续期 TLS 证书来承担这一角色。本指南先介绍让你的应用感知代理的 Strapi 配置,然后是将来流量路由到它的 Caddyfile。Strapi 的更改属于你的项目,因此请在部署前完成。Caddy 的更改则在运行 Caddy 的机器或容器上进行。

WARNING
  • 一个能够在本地启动并运行的 Strapi 5 应用(参见 部署指南)。
  • Caddy 运行在与 Strapi 相同的主机上,或作为同一 Docker 网络中的容器运行(参见 Caddy 安装文档)。
  • 一个 DNS A 记录指向该服务器的域名。
  • 80 和 443 端口对公网开放。Caddy 需要 80 端口来完成证书挑战。
  • 具有 sudo 权限的 Shell 访问。

为反向代理配置 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'),
  },
});
WARNING

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

信任代理头部

Caddy 会添加一个携带原始客户端 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)时设置。
IP 欺骗

将 proxy.koa 设为 true 但未设置 proxy.maxIpsCount 时,计数会保留其默认值 0,即无限制。请将 maxIpsCount 设为 Strapi 前方代理的真实数量,以便只读取由你自己的基础设施添加的地址。Caddy 默认会丢弃客户端提供的 X-Forwarded-* 值,因此这一点在 Caddy 前方还有代理或 CDN 时最为重要。

Strapi 读取由 proxy.ipHeader 指定的头部,其默认值为 X-Forwarded-For。Caddy 设置的正是这个头部,因此你无需更改它。

Caddy 在此处提供的保护并非每个代理都具备。它会丢弃客户端发送的任何 X-Forwarded-* 值,并写入自己的值,因此客户端无法向链条中注入伪造地址。该保护依赖于 Caddy 是第一个看到请求的代理。如果 Caddy 前方还有 CDN 或负载均衡器,请将其声明为受信任的代理,以便 Caddy 保留上游添加的地址:

{
    servers {
        trusted_proxies static private_ranges
    }
}

api.example.com {
    reverse_proxy 127.0.0.1:1337
}

在 Strapi 一侧设置 proxy.maxIpsCount 时,请将该链条中的每个代理都计算在内。

提高上传的体积上限

默认情况下 Caddy 不限制请求体大小,因此适用的是 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。要更改它,请参阅本地上传提供方配置和最大文件大小。

配置 Caddy

在 Strapi 感知到代理之后,下一步就是将来流量转发到它的 Caddyfile。

编写 Caddyfile

Caddy 从单一文件中读取其配置,默认位于 /etc/caddy/Caddyfile。一个可用的 Strapi 代理只需 3 行:

api.example.com {
    reverse_proxy 127.0.0.1:1337
}

在代码块顶部指定公开域名会触发自动 HTTPS。Caddy 会在首次启动时为 api.example.com 获取 Let's Encrypt 证书,将 HTTP 重定向到 HTTPS,并在证书过期前进行续期。配置中不会出现证书路径。

reverse_proxy 指令会自动设置 X-Forwarded-For、X-Forwarded-Proto 和 X-Forwarded-Host,这就是为什么代码块如此简短。这些正是上面 Strapi proxy 选项所依赖的头部。WebSocket 连接也会在无需额外配置的情况下被代理,远程数据传输 功能就依赖于此。

NOTE

Caddyfile 是大多数部署使用的格式,但并非唯一格式。Caddy 的原生配置格式是 JSON,config adapters 可将 YAML、TOML、HCL、CUE,甚至已有的 Nginx 配置转换为 JSON。使用 --adapter 标志选择格式,例如 caddy run --config caddy.yaml --adapter yaml。完整列表请参阅 Caddy config adapters 文档。无论你选择哪种格式,本指南中的 Strapi 选项都是相同的。

重新加载 Caddy 以应用该文件:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

caddy validate 命令会在你重新加载之前检查配置。重新加载错误的配置会导致站点宕机,因此请勿跳过这一步。

设置上传大小上限

除非另行指定,Caddy 接受任意大小的请求体。在 /etc/caddy/Caddyfile 中设置一个明确的上限,可以在代理层就拒绝过大的上传,而不必等到 Strapi 缓冲之后:

api.example.com {
    request_body {
        max_size 100MB
    }

    reverse_proxy 127.0.0.1:1337
}

请将此值保持在 Strapi body 中间件中的 formidable.maxFileSize 之上或与之相等,否则 Caddy 会拒绝 Strapi 本可以接受的上传。formLimit 不是要比较的对象:它限制的是普通表单字段,而非上传的文件。

NOTE

max_size 子指令自 Caddy v2.3.0 起可用。在更早的版本上,请省略该代码块,改由 Strapi 中的 formidable.maxFileSize 强制执行限制。

代理到运行在容器中的 Strapi

当 Caddy 和 Strapi 都作为容器运行时,reverse_proxy 通过名称指向 Strapi 服务。在 Caddy 容器内部,127.0.0.1 指向的是该容器自身,而非 Strapi。下面的 Caddyfile 由紧随其后的 Compose 文件挂载到容器中:

api.example.com {
    request_body {
        max_size 100MB
    }

    reverse_proxy strapi:1337
}

Strapi 也必须绑定到 0.0.0.0。如果绑定到 localhost,它只接受来自自身容器内部的连接,Caddy 将无法访问它。本指南前面所示的 host 值已经使用了 0.0.0.0。

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

services:
  strapi:
    image: my-strapi-app
    environment:
      HOST: 0.0.0.0
      PORT: 1337
      PUBLIC_URL: https://api.example.com
    # expose 使端口仅在网络内部可达
    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:
WARNING

如上所示,在 /data 挂载一个持久卷。Caddy 将签发的证书存储在那里。如果没有它,每次容器重启都会请求新证书,从而触及 Let's Encrypt 速率限制,导致站点在限制重置之前没有有效证书。

验证代理设置

Strapi 在 /_health 暴露了一个健康检查路由,以 HTTP 204 No Content 和一个 strapi 头部作为响应。通过代理请求它可以确认 Caddy 能够访问 Strapi:

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

正常的设置会返回状态行和头部:

HTTP/2 204
strapi: You are so French!

然后确认链条的其余部分:

  1. 在浏览器中打开 https://api.example.com/admin 并登录。管理面板通过 HTTPS 加载,没有证书警告。
  2. 在媒体库中上传一张图片。其 URL 使用的是你的域名,而非 localhost:1337。
  3. 检查 Strapi 的输出,确认显示的是真实的客户端 IP 地址,而非 127.0.0.1。如果 Strapi 在前台运行,地址会显示在该终端中。

故障排查

以下每个症状都指向设置中的某一方面。症状以粗体显示,其后为导致该症状的原因以及需要更改的内容:

  • 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 读取的是 socket 地址,而非转发的头部。

  • 密码重置邮件链接到 localhost:1337。 url 选项未设置,或仍指向本地地址。请将其设为公开 URL 并重新构建管理面板。

后续步骤

后续步骤

### 在进程管理器下运行 Strapi

查看服务器配置选项

阅读部署指南