# 用 Traefik 代理 Strapi

> Source: https://strapi.nodejs.cn/cms/deployment/guides/traefik

🌐 Proxying Strapi with Traefik

告诉 Strapi 它的公共地址，并且有一个代理在它前面，通过使用 `server.url` 和 `server.proxy` 选项。然后给 Strapi 容器添加 Traefik 标签，让 Traefik 发现它、路由到 1337 端口，并为你的域名请求证书。

Strapi 监听一个普通的 HTTP 端口，本身不处理 TLS。像 Traefik 这样的反向代理会位于它前面处理 HTTPS，在 443 端口提供你的应用，并将请求转发到 Strapi 进程。Traefik 和本节中的其他代理不太一样：它不是通过配置文件来描述路由，而是监控 Docker API 并从每个容器的标签中读取路由规则。这种方式适合容器频繁增删的部署。本文指南会先讲如何配置 Strapi 让你的应用能识别代理，再讲 Traefik 的设置如何把流量路由到它。

🌐 Strapi listens on a plain HTTP port and does not terminate TLS itself. A reverse proxy such as Traefik sits in front of it to handle HTTPS, serve your application on port 443, and forward requests to the Strapi process. Traefik differs from the other proxies in this section: instead of a configuration file describing your routes, it watches the Docker API and reads routing rules from labels on each container. That suits deployments where containers come and go. This guide covers the Strapi configuration that makes your application proxy-aware, then the Traefik setup that routes traffic to it.

:::prerequisites

- 一个可以在本地启动和运行的 Strapi 5 应用（参见[部署指南](/cms/deployment#general-guidelines)）。
- Strapi 已打包为容器镜像（见 [Docker 安装](/cms/installation/docker)）。
- [Docker Compose](https://docs.docker.com/compose/) 已安装在主机上。
- 一个 DNS `A` 记录指向该主机的域名。
- 端口 80 和 443 对公共互联网开放。Traefik 需要其中一个来完成证书验证。

:::

## 为反向代理配置 Strapi {#configure-strapi-for-a-reverse-proxy}

🌐 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}

🌐 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:

```js title="/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'),
  },
});
```
```ts title="/config/server.ts"

  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 无法访问它。

🌐 The `host` value must stay `0.0.0.0`. Bound to `localhost`, Strapi accepts connections only from inside its own container and Traefik cannot reach it.

:::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, then rebuild your image.

:::

### 信任代理头 {#trust-the-proxy-headers}

🌐 Trust the proxy headers

Traefik 会添加一个 `X-Forwarded-For` 头，其中包含原始客户端的 IP 地址。Strapi 默认会忽略这个头，除非你开启代理支持。

🌐 Traefik 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:

```js title="/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'),
  // highlight-start
  proxy: {
    koa: true,
    maxIpsCount: 1,
  },
  // highlight-end
  app: {
    keys: env.array('APP_KEYS'),
  },
});
```
```ts title="/config/server.ts"

  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'),
  },
});
```

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

🌐 Each option plays a different role:

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

:::warning IP spoofing

在没有设置 `proxy.maxIpsCount` 的情况下，将 `proxy.koa` 设置为 `true` 会让计数保持默认的 `0`，也就是无限制。把 `maxIpsCount` 设置为 Strapi 前面代理的实际数量，这样就只会读取你自己基础设施添加的地址。

🌐 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.

:::

Strapi 会读取名为 `proxy.ipHeader` 的请求头，默认是 `X-Forwarded-For`，而 Traefik 会设置同样的请求头。Traefik 还会设置 `X-Forwarded-Proto`，Strapi 用它来把管理面板的刷新令牌 cookie 标记为 `Secure`。密码重置、登录提供者回调和媒体资源的绝对 URL 是从 `url` 选项构建的，而不是从那个请求头获取。

🌐 Strapi reads the header named by `proxy.ipHeader`, which defaults to `X-Forwarded-For`, and Traefik sets that same header. Traefik also sets `X-Forwarded-Proto`, which Strapi uses to mark the admin panel refresh-token cookie as `Secure`. Absolute URLs for password resets, login provider callbacks, and media assets are built from the `url` option instead, not from that header.

Traefik 在这里的作用不是每个代理都能做到的。默认情况下，它会清理客户端发送的 `X-Forwarded-*` 头，并写入自己的头，所以客户端无法在链中注入假的地址。这种保护依赖于 Traefik 是看到请求的第一个代理。如果 CDN 或负载均衡器在 Traefik 前面，就在入口点的 `forwardedHeaders.trustedIPs` 中列出它们的地址，这样 Traefik 就能保留上游添加的内容，然后在设置 `proxy.maxIpsCount` 时计算链中每个代理的数量：

🌐 Traefik helps here in a way that not every proxy does. By default it sanitizes the `X-Forwarded-*` headers a client sends and writes its own, so a client cannot inject a fake address into the chain. That protection depends on Traefik being the first proxy to see the request. If a CDN or load balancer sits in front of Traefik, list its addresses in the entry point's `forwardedHeaders.trustedIPs` so Traefik preserves what that upstream added, then count every proxy in the chain when you set `proxy.maxIpsCount`:

```yml title="./traefik/traefik.yml"
entryPoints:
  websecure:
    address: ':443'
    forwardedHeaders:
      trustedIPs:
        - '203.0.113.0/24'
```

### 提高上传的文件大小限制 {#raise-the-body-size-limits-for-uploads}

🌐 Raise the body size limits for uploads

Traefik 会把请求体直接传到后端，不会有大小限制，除非你自己加一个，所以默认情况下是 Strapi 的限制生效。如果你通过媒体库上传文件，就把限制提高一下吧。

🌐 Traefik streams request bodies through to the backend without a size cap unless you add one, so the Strapi limits are the ones that apply by default. 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:

```js title="/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。要更改它，请参阅 [本地上传提供者配置](/cms/configurations/media-library-providers/local-upload) 和 [最大文件大小](/cms/features/media-library#max-file-size)。

🌐 The Media Library provider enforces a separate `sizeLimit`, which defaults to 1 GB. To change it, see [local upload provider configuration](/cms/configurations/media-library-providers/local-upload) and [max file size](/cms/features/media-library#max-file-size).

## 配置 Traefik {#configure-traefik}

🌐 Configure Traefik

Traefik 读取两种配置。静态配置定义入口点和证书解析器，只在 Traefik 启动时设置一次。动态配置定义路由，来自容器标签，并会在容器出现时被自动识别。

🌐 Traefik reads 2 kinds of configuration. Static configuration, which defines entry points and certificate resolvers, is set once when Traefik starts. Dynamic configuration, which defines routes, comes from container labels and is picked up as containers appear.

### 写静态配置 {#write-the-static-configuration}

🌐 Write the static configuration

以下文件在每个公共端口定义了一个入口点，将 HTTP 重定向到 HTTPS，并注册了一个 Let's Encrypt 证书解析器：

🌐 The following file defines an entry point on each public port, redirects HTTP to HTTPS, and registers a Let's Encrypt certificate resolver:

```yml title="./traefik/traefik.yml"
entryPoints:
  web:
    address: ':80'
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ':443'

providers:
  docker:
    # Only route to containers that opt in with traefik.enable=true
    exposedByDefault: false

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /acme/acme.json
      tlsChallenge: {}
```

将 `exposedByDefault` 设置为 `false` 很重要。如果保持默认设置，Traefik 会发布它能看到的每个容器，包括那些你根本不想暴露的容器。

🌐 Setting `exposedByDefault` to `false` matters. Left at its default, Traefik publishes every container it can see, including ones you never meant to expose.

### 用标签将流量路由到 Strapi {#route-traffic-to-strapi-with-labels}

🌐 Route traffic to Strapi with labels

在静态配置就位后，Strapi 容器通过标签声明了自己的路由：

🌐 With the static configuration in place, the Strapi container declares its own route through labels:

```yml title="./docker-compose.yml"
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 就不会暴露在公共接口上。

🌐 The `loadbalancer.server.port` label is the one people miss. Traefik routes to a port inside the container network, so the Strapi container needs no `ports` mapping of its own. Only Traefik publishes ports to the host, which keeps Strapi off the public interface.

:::warning

这个文件的两个部分会带来超出 Traefik 本身的后果：

- **为证书存储路径挂载一个持久卷**，如上面用 `traefik_acme` 所示。Traefik 会将下发的证书存储在 `acme.json`。如果没有它，每次容器重启都会请求新的证书，这会达到  [Let](https://letsencrypt.org/docs/rate-limits/)  并且在限制重置之前让网站没有有效证书。
- **挂载 Docker 套接字会让 Traefik 完全访问 Docker API**，这相当于在主机上拥有 root 权限。上面显示的 `:ro` 标志是针对套接字文件本身的，而不是 API，所以它不会限制 Traefik 通过套接字能做的事情。把它当作一种约定，而不是限制。不要公开暴露 Traefik 仪表盘，如果要进行安全加固部署，把套接字放在一个只暴露 Traefik 所需端点的代理后面。

:::

### 限制请求体大小 {#cap-the-request-body-size}

🌐 Cap the request body size

Traefik 默认不限制请求大小。想要在代理处而不是 Strapi 拒绝过大的上传，可以在 Strapi 容器的标签中添加 `buffering` 中间件：

🌐 Traefik applies no request size limit by default. To reject oversized uploads at the proxy instead of at Strapi, add the `buffering` middleware to the Strapi container's labels:

```yml title="./docker-compose.yml"
    labels:
      # ...
      - 'traefik.http.middlewares.strapi-limit.buffering.maxRequestBodyBytes=104857600'
      - 'traefik.http.routers.strapi.middlewares=strapi-limit'
```

超过限制的请求会收到 `413` 响应。

🌐 A request over the limit receives a `413` response.

:::note

`buffering` 中间件会在转发请求之前读取整个请求体，如果超过阈值会溢写到磁盘上。这会增加每次上传的延迟和磁盘使用，对于接收大文件的媒体库来说，这样的代价不太值得。如果你的上传文件很大，关闭这个中间件，让 Strapi 里的 `formidable.maxFileSize` 来控制限制就行。

🌐 The `buffering` middleware reads the entire request body before forwarding it, spilling to disk beyond a threshold. That adds latency and disk usage on every upload, which is a poor trade for a Media Library that accepts large files. If your uploads are large, leave this middleware off and let `formidable.maxFileSize` in Strapi enforce the limit instead.

:::

## 检查代理设置 {#verify-the-proxy-setup}

🌐 Verify the proxy setup

启动堆栈并检查 Traefik 是否能够访问 Strapi。Strapi 提供了一个健康检查路由 `/_health`，会返回 HTTP `204 No Content` 和一个 `strapi` 头:

🌐 Bring up the stack and check that Traefik reaches Strapi. Strapi exposes a health check route at `/_health` that responds with HTTP `204 No Content` and a `strapi` header:

```bash
docker compose up -d
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 容器日志，用 `docker compose logs strapi` 获取真实客户端 IP 地址，而不是 Traefik 容器地址。

##  故障排除 {#icon-namebug--troubleshooting}

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

🌐 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:

- **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`。
- **没有颁发证书。** 证书验证需要从公网可以访问端口 443，用于 `tlsChallenge`，而域名的 DNS `A` 记录必须已经解析到这台主机。使用 `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，重建管理面板，并重新构建镜像。

## 下一步 {#next-steps}

🌐 Next steps

<NextSteps title="">
  <NextSteps.Step
    title="Review the server configuration options"
    description="The full list of options available in the server config file."
    link="/cms/configurations/server"
  />
  <NextSteps.Step
    title="Build the Strapi image"
    description="The Docker installation guide covers the image this guide routes to."
    link="/cms/installation/docker"
  />
