使用 Nginx 代理 Strapi

页面摘要: 使用 server.url 和 server.proxy 选项告知 Strapi 它的公开地址以及前方存在代理。然后添加一个 Nginx server 块,将请求连同原始客户端信息一起转发给 Strapi。

Strapi 监听一个普通的 HTTP 端口,自身并不终止 TLS。诸如 Nginx 这样的反向代理位于其前方,用于处理 HTTPS、在 443 端口提供你的应用,并将请求转发给 Strapi 进程。本指南涵盖设置的两个部分:让你的应用感知代理的 Strapi 配置,以及将来流量路由到它的 Nginx 配置。Strapi 的更改属于你的项目,因此请在部署前完成。Nginx 的更改则在运行 Nginx 的机器或容器上进行。

WARNING
  • 一个能够在本地启动并运行的 Strapi 5 应用(参见 部署指南)。
  • Nginx 运行在与 Strapi 相同的主机上,或作为同一 Docker 网络中的容器运行(参见 Nginx 安装文档)。
  • 一个 DNS A 记录指向该服务器的域名。
  • 具有 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。

信任代理头部

Nginx 会添加一个携带原始客户端 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,即无限制。这样一来,客户端可以发送 X-Forwarded-For: 203.0.113.9,一旦 Nginx 附加上真实地址,Strapi 会从链条前端读取伪造的值,而不是末尾的真实地址。请始终将 maxIpsCount 设为 Strapi 前方代理的真实数量。

Strapi 读取由 proxy.ipHeader 指定的头部,其默认值为 X-Forwarded-For。下面的 Nginx 配置设置的正是这个头部,因此你无需更改它。只有当更上游的代理使用了不同的名称(例如 Cloudflare 背后的 CF-Connecting-IP)时,才需要覆盖它。

提高上传的体积上限

Nginx 和 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。要更改它,请参阅本地上传提供方配置和最大文件大小。

配置 Nginx

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

创建 server 块

为你的站点创建一个配置文件:

# Send the upgrade hint only on requests that actually ask for it
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name api.example.com;

    # Raise this to match the largest upload you accept
    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:1337;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

X-Forwarded-Proto 头部告知 Strapi 原始请求是通过 HTTPS 到达的。Strapi 使用它来将管理面板的刷新令牌 cookie 标记为 Secure。密码重置、登录提供方回调以及媒体资源的绝对 URL 是基于 url 选项构建的,而非基于此头部。

启用站点并重新加载 Nginx。下面的 sites-available 和 sites-enabled 布局是 Debian 和 Ubuntu 的惯例。在基于 RHEL 的发行版上,请将文件放在 /etc/nginx/conf.d/ 中,并跳过符号链接步骤:

sudo ln -s /etc/nginx/sites-available/strapi.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t 命令会在你重新加载之前校验配置。重新加载错误的配置会导致站点宕机,因此请勿跳过这一步。

上面的 map 块只在请求自身要求升级时才转发连接升级,而不是为每一个被代理的请求都附加升级提示。Strapi 的远程数据传输 功能需要它,该功能在你对远程实例运行 strapi transfer 时会打开一个 WebSocket 连接。

代理到运行在容器中的 Strapi

当 Nginx 和 Strapi 都作为容器运行时,有 2 个细节会发生变化。

首先,proxy_pass 通过服务名称而非回环地址指向 Strapi 服务。在 Nginx 容器内部,127.0.0.1 指向的是该容器自身,而非 Strapi。Docker 在共享网络上解析服务名称,因此服务名称才是能够到达 Strapi 的地址:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name api.example.com;

    client_max_body_size 100M;

    location / {
        proxy_pass http://strapi:1337;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

其次,Strapi 必须绑定到 0.0.0.0。如果绑定到 localhost,它只接受来自自身容器内部的连接,Nginx 将无法访问它。本指南前面所示的 host 值已经使用了 0.0.0.0,这正是容器可达的原因。

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

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

  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/strapi.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - strapi
    networks:
      - web

networks:
  web:

对 Strapi 服务使用 expose 而非 ports,可使其不暴露到公共接口,因此流量只能通过 Nginx 到达。

终止 TLS

到目前为止展示的 server 块监听的是 80 端口。生产环境部署需要证书以及 443 端口上的监听器。Nginx 自身不会获取证书,因此你需要将其与能够签发并续期证书的工具搭配使用,或者在流量到达 Nginx 之前就终止 TLS。

以下选项都很常用:

选项适用场景
CertbotLet's Encrypt 官方参考客户端。其 Nginx 插件可以重写 server 块并为你配置续期。
acme.sh一个无 Python 依赖的 shell 客户端,支持广泛的 DNS 提供方,在需要通过 DNS-01 挑战获取通配符证书时尤为重要。
lego单个 Go 二进制文件,在最小化镜像和容器构建中很有用。
nginx-proxy 与 acme-companion容器部署场景,证书按容器签发,无需手动编辑配置。
云负载均衡器或 CDNTLS 在上游终止的场景,例如由 AWS Certificate Manager、Cloudflare 或你的托管提供方的负载均衡器终止。

无论你选择哪一种,最终 server 块都会监听 443 并指向证书文件。请用以下内容替换上面创建的 80 端口块,其中保留了一个 80 端口的第二个块,以便将纯 HTTP 请求重定向,而不是直接到达 Strapi:

# Send the upgrade hint only on requests that actually ask for it
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name api.example.com;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    # Raise this to match the largest upload you accept
    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:1337;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

自 Nginx 1.25.1 起,http2 on; 是一个独立的指令。在更早的版本上,应写为 listen 443 ssl http2;。证书文件就位后重新加载 Nginx,下面的检查将针对 HTTPS 地址进行。

WARNING

如果 TLS 由上游的负载均衡器或 CDN 终止,Nginx 可以继续监听 80 端口。但该上游仍必须发送 X-Forwarded-Proto: https,否则 Strapi 会将请求视为纯 HTTP 并生成 http:// 形式的 URL。设置 proxy.maxIpsCount 时,请将链条中的每个代理都计算在内。

验证代理设置

Strapi 在 /_health 暴露了一个健康检查路由,以 HTTP 204 No Content 和一个 strapi 头部作为响应。通过代理请求它可以确认 Nginx 能够访问 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 在前台运行,地址会显示在该终端中。

故障排查

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

  • Nginx 返回 502 Bad Gateway。 Nginx 无法访问 Strapi。请确认 Strapi 进程正在运行并监听 proxy_pass 中使用的端口。然后检查 /config/server.js 中的 host 没有绑定到 Nginx 无法访问的网络接口。

  • 上传失败并显示 413 Request Entity Too Large。 请求超出了大小限制。请提高 Nginx 中的 client_max_body_size 以及 Strapi body 中间件中的 formidable.maxFileSize;如果文件大于 1 GB,还要提高提供方的 sizeLimit。三者都必须允许该文件。

  • Strapi 将 127.0.0.1 记录为客户端 IP。 proxy.koa 未设为 true,因此 Strapi 读取的是 socket 地址,而非转发的头部。

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

  • 管理面板的刷新 cookie 未被标记为 Secure。 Nginx 没有转发 X-Forwarded-Proto,因此 Strapi 将请求视为纯 HTTP。该 cookie 仍会通过 HTTPS 到达浏览器,因为它默认为 SameSite=Lax,但它失去了将其排除在纯 HTTP 之外的 Secure 标志。请将该头部添加到 location 块中。

后续步骤

后续步骤

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

查看服务器配置选项

阅读部署指南