Nginx反向代理怎么配置?多站点部署与转发规则详解

一台服务器上跑了三个服务:官网程序监听在 8080,管理后台是另一个语言写的跑在 3000 端口,还有个接口服务在 9001。这些端口都不想暴露给用户,用户只知道 example.com 这一个域名。解决办法就是反向代理——让用户统一访问 Nginx 的 80 和 443,由 Nginx 根据域名或 URL 路径,把请求转发到内部对应的服务。

反向代理看起来只是一个 proxy_pass 指令,实际用起来坑不少:末尾斜杠差一个字符转发路径就全错,不加请求头后端拿不到真实客户端 IP,WebSocket 握手会失败,转发的动态页面里的跳转会跳到内部地址。这篇文章把这些细节逐个讲清楚,并给出可以直接复制改的配置模板。

文中命令区分 RHEL、CentOS、Rocky 系(dnf 或 yum,配置放在 /etc/nginx/conf.d/,防火墙 firewalld)与 Debian、Ubuntu 系(apt,配置常放 /etc/nginx/sites-available/ 并通过符号链接启用,防火墙 ufw)。

反向代理是什么,和正向代理有什么区别

正向代理代理客户端。客户端主动配置代理服务器,由代理替它去访问目标网站。正向代理隐藏的是客户端身份,典型例子是内网出口代理。

反向代理代理服务端。客户端对此毫不知情,它以为自己访问的就是目标服务器本身,实际上请求被代理接收后转发给了后端真实服务。反向代理隐藏的是服务端结构。

对一个网站来说,反向代理带来几个直接收益:

统一入口。 外部只暴露 80 和 443,内部服务可以随意使用非标准端口。这既减少了对外暴露的攻击面,也让端口管理不再混乱。相反,如果每个服务各自对外开一个端口,防火墙规则和用户书签都会失控。

集中 TLS 终结。 证书只在 Nginx 上配置一次,Nginx 与后端之间走内网 HTTP。后端服务不需要各自管理证书,也不用处理 TLS 握手开销。

负载均衡。 同一个后端可以有多个实例,Nginx 按策略分发,某个实例故障时自动摘除。

缓冲与容错。 Nginx 可以先完整接收后端响应再发给客户端,这样慢速客户端不会长期占用后端进程——后端快速把响应交给 Nginx 就能继续服务下一个请求。

静态资源与压缩、缓存下沉到边缘。 静态文件由 Nginx 直接返回,不必劳烦后端应用。

代价也要清楚:多一跳会带来少量延迟,通常在同一内网里只有零点几到几毫秒的量级;日志里如果不做处理,记录的客户端 IP 会全是 Nginx 自身的地址;出问题时排查路径变长,需要在 Nginx 日志和后端日志之间对照。

proxy_pass 语法与末尾斜杠的坑

proxy_pass 后面的 URL 是否带 URI 路径,决定了 Nginx 如何改写请求 URI。这是最容易出错的地方,必须记住规则。

规则一:proxy_pass 不带 URI(只有协议、主机、端口),Nginx 会把完整的原始请求 URI 原样转发过去。

location /api/ {
    proxy_pass http://127.0.0.1:9001;   # 没有末尾斜杠,不带 URI
}

请求 /api/user/list 转发出去是 http://127.0.0.1:9001/api/user/list,location 的前缀被保留了。

规则二:proxy_pass 带了 URI(哪怕只是一个斜杠),Nginx 会把 location 中匹配的部分替换成该 URI。

location /api/ {
    proxy_pass http://127.0.0.1:9001/;   # 注意末尾的斜杠
}

请求 /api/user/list 转发出去是 http://127.0.0.1:9001/user/list,location 前缀 /api/ 被替换成了 /。

再举一个带具体路径的例子:

location /api/ {
    proxy_pass http://127.0.0.1:9001/v2/;
}

请求 /api/user/list 变成 http://127.0.0.1:9001/v2/user/list。

用一张表对照:

location proxy_pass 请求 URI 转发后的 URI
/api/ http://127.0.0.1:9001 /api/user/list /api/user/list
/api/ http://127.0.0.1:9001/ /api/user/list /user/list
/api/ http://127.0.0.1:9001/v2/ /api/user/list /v2/user/list
/api http://127.0.0.1:9001/ /api/user/list /user/list

实际场景中的选择很简单:如果后端服务的路径规划和外部 URL 完全一致,用不带斜杠的形式;如果后端本身不带 /api 前缀(比如后端直接提供 /user/list),就用带斜杠的形式让它被剥离。

还有一个常被忽略的限制:在带变量的 proxy_pass 里(比如 proxy_pass http://$backend$request_uri;),行为又有差异,且如果使用了变量并指向 upstream,需要配合 resolver 指定 DNS 服务器,否则会报"no resolver defined"错误。除非确实需要动态计算后端地址,否则不要引入变量。

必须设置的请求头

后端看不到客户端的真实信息,这是反向代理最常见的问题:程序记录的 IP 全是 127.0.0.1,生成的链接是 http 而不是 https,或者重定向到了内部域名。解决办法是显式传递请求头。

location / {
    proxy_pass http://backend_app;

    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 X-Forwarded-Host  $host;
}

逐个说明:

Host。 后端如果需要根据域名做虚拟主机路由或生成绝对链接,必须拿到客户端请求的原始 Host。不设置时 Nginx 默认传的是 proxy_pass 里的主机名(如 backend_app 或 127.0.0.1:9001),会导致后端匹配错虚拟主机。变量方面,$host 是不带端口的规范化主机名,$http_host 保留原始请求里的 Host(含端口),多数情况用 $host 即可。

X-Real-IP。 直接给后端一个"客户端 IP"字段,值为 $remote_addr,即与 Nginx 建立 TCP 连接的对端地址。

X-Forwarded-For(简称 XFF)。 格式为逗号加空格分隔的 IP 列表,每经过一层代理就追加一个。用 $proxy_add_x_forwarded_for 而不是 $remote_addr 的原因就在这里:它会保留客户端传来的已有值并在后面追加当前对端地址。取真实 IP 时应从严到宽地解析这个链表,而不是取第一个值——第一个值是客户端可以伪造的。

X-Forwarded-Proto。 值为 $scheme,也就是 http 或 https。这是关键一项:Nginx 终结 TLS 后到后端走的是 HTTP,后端以为请求是明文的,于是生成 http:// 的链接或拒绝会话 Cookie,导致混合内容警告或反复重定向。凡是"开了 HTTPS 之后后台登录循环跳转"的问题,优先查这一项。

Nginx 侧还有一个配套模块 ngx_http_realip_module,可以在 Nginx 这一层就把 $remote_addr 改写成真实 IP,这样日志和后端都无需特殊处理:

set_real_ip_from 10.0.0.0/8;          # 信任的上游代理网段
set_real_ip_from 127.0.0.1;
real_ip_header   X-Forwarded-For;     # 从哪个头取真实 IP
real_ip_recursive on;                 # 递归剥离受信任地址,取最后一个不受信任的

安全提醒:set_real_ip_from 只应填写你确实控制的可信代理地址。如果随意信任任意来源,攻击者伪造一个 XFF 头就能让 Nginx 记录错误的客户端 IP,进而绕过基于 IP 的访问控制。

用 upstream 定义后端组

直接在 proxy_pass 里写 IP 加端口能工作,但不利于扩展。定义 upstream 可以复用、可以配置多台做负载均衡、可以做健康检查参数。

upstream backend_app {
    server 127.0.0.1:9001 weight=3 max_fails=3 fail_timeout=10s;
    server 127.0.0.1:9002 weight=1 max_fails=3 fail_timeout=10s;
    server 127.0.0.1:9003 backup;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";      # 配合 upstream keepalive 复用连接
        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;
    }
}

server 指令的常用参数:

参数 含义 备注
weight=N 轮询权重,默认为 1 新机器性能好可以给更高权重
max_fails=N 在 fail_timeout 内失败 N 次判定为不可用 与 fail_timeout 配合使用
fail_timeout=N 判定时长窗口,也是标记为不可用后的暂停时长 单位支持 s、m
backup 备份节点,主节点全不可用时才启用 适合做低成本灾备
down 标记为永久不可用,常用于临时摘除 手动维护某台机器时使用

注意 upstream 中的 keepalive 与 proxy_http_version 1.1 加 proxy_set_header Connection "" 是配套的三件套,用来复用 Nginx 到后端的连接,减少握手开销。如果缺少后面两项,连接复用不会生效。

Nginx 开源版的健康检查是被动式的:请求转发失败达到阈值才判定节点故障。主动探测(health_check 指令)属于商业版或第三方模块能力。

负载均衡策略怎么选

策略 指令 行为 适合场景
轮询 默认 按权重依次分发 后端无状态、性能一致
最少连接 least_conn; 优先给当前活跃连接最少的节点 请求处理时长差异大,比如有快慢两种接口
源地址哈希 ip_hash; 按客户端 IP 前三段哈希固定到某节点 无集中会话存储时的简单会话保持
一致性哈希 hash $request_uri consistent; 按指定变量哈希,节点增减时影响范围小 需要缓存亲和性的场景
随机 random two least_conn; 随机挑两个再取连接少的 大规模节点时的轻量方案
upstream backend_app {
    least_conn;
    server 127.0.0.1:9001;
    server 127.0.0.1:9002;
}

选策略的核心原则:优先让应用本身做到无状态(会话放 Redis 或数据库,文件放对象存储),这样默认轮询就够用,扩缩容最自由。只有当短期无法改造时,才用 ip_hash 之类的方案做会话保持——它的问题在于同一出口 IP 的用户(比如公司 NAT 后面的人)会全被分到同一台机器,负载可能严重不均。

超时与缓冲参数

默认超时值偏保守,遇到慢接口要显式调整。

location /api/ {
    proxy_pass http://backend_app;

    proxy_connect_timeout 5s;      # 与后端建立 TCP 连接的超时,不宜过长
    proxy_send_timeout    30s;     # 向后端发送请求的超时
    proxy_read_timeout    60s;     # 等待后端响应的超时,按业务最慢接口调整
}

参数含义与调整思路:

参数 默认值 作用 调整建议
proxy_connect_timeout 60s 建立与后端连接的时限 一般 2 到 5 秒足够,太长会让故障节点拖住请求
proxy_send_timeout 60s 两次写操作之间的间隔上限 通常保持默认或按需微调
proxy_read_timeout 60s 两次读操作之间的间隔上限 长任务接口需调大,但要配合后端超时一起改,否则前后端不一致会出现重复提交

这里有一个常见误区:把 proxy_read_timeout 调到很大来"解决"502 超时问题。如果一个接口原本几秒返回、现在要几十秒,真正要查的是数据库慢查询或外部 API 卡顿,把超时调大只会让请求堆积、连接池耗尽,把局部问题放大成全局故障。超时值应该略大于业务的 P99 响应时间,而不是无限大。

超时导致的错误对应关系:

proxy_connect_timeout 超时 → 504 Gateway Time-out(Nginx 连接不上后端)
proxy_read_timeout    超时 → 504 Gateway Time-out
后端返回无效或未响应   → 502 Bad Gateway
后端返回内容过大Nginx缓冲不足 → 可能 502 并记录相关错误

缓冲相关配置:

proxy_buffering      on;            # 默认开启,先收后端响应再发给客户端
proxy_buffer_size    8k;            # 用于存放响应头的缓冲区
proxy_buffers        8 8k;          # 响应体的缓冲区数量与大小
proxy_busy_buffers_size 16k;
proxy_max_temp_file_size 0;         # 禁止缓冲落盘,避免磁盘 IO 抖动

开启缓冲能让后端快速释放,这是它的价值。但对于服务端推送、SSE、大文件流式下载这类场景必须关掉,否则客户端要等整个响应收完才收到数据:

location /stream/ {
    proxy_pass http://backend_app;
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
    proxy_read_timeout 3600s;
}

WebSocket 代理

WebSocket 通过 HTTP 的 Upgrade 机制建立,Nginx 默认不会自动转发相关请求头,需要显式处理。

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

upstream ws_backend {
    server 127.0.0.1:9501;
}

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

    location / {
        proxy_pass http://ws_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host       $host;
        proxy_read_timeout 3600s;         # 长连接要放宽读超时
        proxy_send_timeout 3600s;
    }
}

用 map 而不是直接写死 Connection "upgrade" 的原因是这样更严谨:只有客户端确实带了 Upgrade 头时才转发升级请求,普通 HTTP 请求保持 close,避免把非 WebSocket 请求也当成升级请求发出去,某些后端会因此报错。

proxy_read_timeout 必须放宽,因为 WebSocket 大部分时间是空闲的,默认 60 秒会被 Nginx 主动断开,表现为"每隔一分钟连接就掉一次",这是 WebSocket 配置里最高频的问题。

静态资源分离

动态应用处理图片、CSS、JS 是很浪费的——后端进程数量有限,把它们占用来传输静态字节很不合算。让 Nginx 直接返回静态文件,后端只处理真正需要计算的请求:

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com/public;

    # 静态资源:本地存在就直接返回,带长缓存头
    location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|css|js|woff2|ttf)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        try_files $uri =404;
    }

    # 带版本号的构建产物可以设更长的缓存
    location ^~ /assets/ {
        expires 1y;
        access_log off;
        try_files $uri =404;
    }

    # 其余请求转发给后端
    location / {
        proxy_pass http://backend_app;
        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;
    }
}

access_log off 关掉静态资源日志能显著降低磁盘写入,日志量大的站点效果明显。带内容哈希的文件名(如 app.a1b2c3.js)可以放心用一年缓存,因为内容变化时文件名也会变。

路径转发还是域名转发

多站点部署有两种基本划分方式,各有适用情况。

按域名转发——不同域名指向不同后端:

server {
    listen 80;
    server_name www.example.com;
    location / { proxy_pass http://web_backend; }
}

server {
    listen 80;
    server_name api.example.com;
    location / { proxy_pass http://api_backend; }
}

优点是每个服务路径干净,不需要处理前缀剥离,Cookie 作用域、跨域策略都好写。缺点是需要为每个子域添加 DNS 记录。

按路径转发——一个域名下用不同前缀区分:

server {
    listen 80;
    server_name example.com;

    location /api/   { proxy_pass http://api_backend/; }
    location /admin/ { proxy_pass http://admin_backend/; }
    location /       { proxy_pass http://web_backend; }
}

优点是省 DNS 记录,前端统一域名天然同源,不需要处理跨域。缺点是后端程序里写死的绝对路径(比如