POST · DevOps · 文章
nginx 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 2 次 · 7522 字 · 预计阅读 19 分钟
nginx 从入门到进阶
本文面向已经会部署前端项目、但没系统学过 nginx 的开发者。内容按"先能用,再理解,最后调优"的顺序组织,覆盖静态站点、反向代理、负载均衡、HTTPS、缓存、限流与排错。
文中所有配置都放在配套示例工程里,可以直接启动验证(见附录 A)。标注为"实测"的结果,均来自该示例的运行输出,命令附在对应小节,读者可以自行复现。
- 适用对象:需要部署前端产物、反向代理后端接口、处理 HTTPS 的开发者
- 运行环境:Docker(示例使用 nginx 1.27-alpine、Node 20);Linux 包管理安装方式在第二节说明
- 前置知识:HTTP 基础(请求方法、状态码、请求头/响应头)
一、nginx 是什么
1.1 定位
nginx 是一个 HTTP 服务器和反向代理服务器。请求到达业务代码之前,通常先经过它:
浏览器 ──► nginx ──► 静态文件(直接返回)
└──► 后端服务(反向代理后返回结果)
它承担的是"接入层"职责:静态资源托管、请求转发、TLS 卸载、压缩、缓存、限流、负载均衡。业务逻辑仍由后端服务处理。
1.2 进程模型
┌──────────────┐
│ master │ 读取配置、管理 worker、处理信号
└──────┬───────┘
┌──────────┼──────────┐
┌────▼────┐ ┌───▼─────┐ ┌──▼──────┐
│ worker1 │ │ worker2 │ │ workerN │ 实际处理请求,数量通常等于 CPU 核数
└─────────┘ └─────────┘ └─────────┘
两个直接结论:
- reload 不中断服务:master 加载新配置后启动新 worker,旧 worker 处理完手头请求再退出,所以线上修改配置不需要停服。
- 高并发来自事件驱动:每个 worker 使用 epoll/kqueue 处理连接,单 worker 可维持数千连接,不需要一个请求一个线程。
1.3 反向代理与正向代理
| 由谁部署 | 代理对象 | 典型场景 | |
|---|---|---|---|
| 正向代理 | 客户端 | 客户端 | 企业出口代理 |
| 反向代理 | 服务端 | 服务器 | nginx 将 /api 转发给后端 |
nginx 做的是反向代理:客户端以为在和 nginx 通信,实际响应由后端产生。
1.4 能力边界
| 适合交给 nginx | 不适合交给 nginx |
|---|---|
| 静态资源托管 | 复杂业务逻辑(鉴权、数据拼装) |
| 反向代理、隐藏后端端口 | 作为应用服务器运行 Node/PHP |
| 负载均衡 | 大量条件分支的 rewrite 规则 |
| TLS 终止、HTTP/2 | 需要读写数据库的操作 |
| 压缩、缓存、限流 | 实时计算、长事务 |
常见的过度设计是把业务判断写进 nginx(大量 if 和 rewrite),后期难以维护。能在应用层解决的问题尽量留在应用层。
二、安装与运行
2.1 Linux(以 Ubuntu 为例)
sudo apt update
sudo apt install -y nginx
nginx -v
systemctl status nginx
目录约定:
/etc/nginx/
├─ nginx.conf # 主配置
├─ conf.d/ # 站点配置(通常被 include)
├─ sites-available/ # Debian/Ubuntu 的站点目录
├─ sites-enabled/ # 软链接到这里才生效
└─ mime.types # 扩展名与 Content-Type 的映射
/var/www/html/ # 默认站点根目录
/var/log/nginx/ # access.log、error.log
建议一个站点一个配置文件,放在 conf.d/ 下。sites-available / sites-enabled 是 Debian 系的约定,用或不用都可以。
2.2 Docker
本地或容器化部署时,直接用官方镜像更省事,也便于和线上保持一致:
docker run --rm -p 8080:80 nginx:1.27-alpine
浏览器访问 http://localhost:8080,看到默认欢迎页即安装成功。完整的多服务示例见附录 A。
2.3 常用命令
sudo nginx -t # 校验配置语法(修改后必做)
sudo nginx -T # 打印 include 展开后的最终配置
sudo nginx -s reload # 热加载
sudo nginx -s reopen # 重新打开日志文件(配合日志切割)
sudo systemctl reload nginx # systemd 管理下的等价写法
nginx -t 只检查语法和文件是否存在,不校验路径语义和正则逻辑。路径写错、正则写错它都会通过,这两类问题通常要到请求发生时才暴露。
nginx -T 用于确认"实际生效的配置",排查"改了没生效"时比逐个文件翻更有效。
三、配置文件的结构
3.1 上下文(context)
nginx 配置是树形结构,指令只能写在允许的上下文里:
main ← 最外层:worker_processes、user、pid
├─ events { } ← 连接相关:worker_connections
└─ http { ← HTTP 相关:log_format、gzip、upstream
├─ server { } ← 一个虚拟主机(域名 + 端口)
│ └─ location { } ← 一个 URL 前缀或正则
└─ server { }
}
例如把 gzip on; 写进 server 块,nginx -t 会报 "gzip" directive is not allowed here,这就是上下文限制。
3.2 最小可运行配置
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 80;
server_name _; # _ 表示匹配任意域名
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}
3.3 阅读一份配置的方法
拿到任意 nginx 配置,先回答四个问题:
- 监听哪个端口、哪个域名?——
listen、server_name - 站点根目录在哪?——
root还是alias - 请求由哪个
location处理?—— 匹配规则见第四节 - 处理方式是返回文件还是转发?——
try_files或proxy_pass
四、location 匹配规则
4.1 五种写法
| 写法 | 含义 | 示例 |
|---|---|---|
location = /a |
精确匹配,完全相等才命中 | 只匹配 /a |
location ^~ /static/ |
前缀匹配,命中后不再检查正则 | /static/x.js |
location ~ \.php$ |
正则匹配,区分大小写 | 按书写顺序,先命中先生效 |
location ~* \.(jpg|png)$ |
正则匹配,忽略大小写 | /a.JPG 也命中 |
location /api/ |
普通前缀匹配,最长前缀优先 | /api/、/api/x |
4.2 优先级
1. = 精确匹配 ← 命中即结束
2. ^~ 前缀匹配 ← 命中即结束,不再检查正则
3. ~ / ~* 正则 ← 按书写顺序,第一个命中的生效
4. 普通前缀匹配 ← 选择最长的一个
5. location / ← 兜底
记忆方式:精确 > ^~ > 正则 > 最长前缀 > 兜底。
容易出错的一点:正则优先于普通前缀。因此 location /api/ 会被 location ~ \.json$ 抢占 /api/user.json。接口路径建议写成 location ^~ /api/,明确跳过正则检查。
4.3 验证命中结果
在示例工程中分别请求不同类型的路径,观察状态码即可判断命中了哪个 location:
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/assets/app.js # 200,命中静态资源正则
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/api/info # 200,命中 /api/ 前缀
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/some/spa/route # 200,命中 location /
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/assets/missing.js # 404,命中静态资源正则并 =404
最后一条尤其需要注意:静态资源 location 里如果写成 try_files $uri /index.html,缺失文件会返回 HTML 且状态码为 200,浏览器把 HTML 当 JS 执行后报语法错误。静态资源应使用 =404。
4.4 root 与 alias
# root:把 location 前缀拼到路径后面
location /img/ {
root /data; # 请求 /img/a.png → /data/img/a.png
}
# alias:替换掉 location 前缀
location /img/ {
alias /data/static/; # 请求 /img/a.png → /data/static/a.png
}
两个注意点:
alias结尾必须带/,且要与 location 的前缀对齐,否则路径拼接会出错。alias用在正则 location 中时必须使用捕获组:alias /data/$1;。
实际项目中优先使用 root,可减少一类路径问题。
4.5 try_files
location / {
try_files $uri $uri/ /index.html;
}
含义是按顺序尝试:先找同名文件,再找同名目录,都不存在则内部转发到 /index.html。内部转发不会改变浏览器地址栏,这是单页应用刷新不 404 的实现方式。
实测(示例工程):
curl -s -o /dev/null -w "status=%{http_code} type=%{content_type}\n" https://localhost:8443/some/spa/route
# status=200 type=text/html
五、部署静态站点与单页应用
5.1 文件布局
/data/www/my-app/
├─ index.html
└─ assets/
├─ index-Df76S7DV.js # 文件名带内容 hash
└─ index-Df76S7DV.css
5.2 配置模板
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
root /data/www/my-app;
index index.html;
# 1) 带 hash 的静态资源:长缓存
location ~* \.(?:js|css|woff2?|png|jpe?g|gif|svg|ico|webp)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
access_log off;
try_files $uri =404;
}
# 2) index.html 不缓存,保证版本可更新
location = /index.html {
add_header Cache-Control "no-cache" always;
}
# 3) 单页应用路由回退
location / {
try_files $uri $uri/ /index.html;
}
}
index.html 单独配置的原因:页面里引用的是带 hash 的 JS/CSS,只要 HTML 是最新的,资源自然是最新的。如果 HTML 被长缓存,用户会持续加载旧资源,表现为"发布了新版本但用户仍看到旧页面"。
5.3 验证响应头
curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js
HTTP/1.1 200 OK
ETag: W/"6af70172-77c"
Cache-Control: public, max-age=2592000, immutable
Content-Encoding: gzip
两个常见配置问题:
expires与add_header Cache-Control同时使用时,响应里会出现两个Cache-Control(expires自身也会生成一个max-age)。二者选其一即可。- location 中出现
add_header后,不再继承 server 级的add_header。安全响应头要么写在每个 location 里,要么统一加always参数,不能假设"server 级配置了就在所有 location 生效"。
5.4 gzip
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css application/json
application/javascript application/xml image/svg+xml;
默认 gzip_types 只包含 text/html,JS、CSS、JSON 需要显式添加,否则会出现"配置了 gzip 但没有效果"。
验证要看响应头:
curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js
# Content-Encoding: gzip
brotli 压缩率更高,但官方镜像不包含 brotli 模块,需要自行编译或通过 CDN 提供。gzip 已能满足大多数场景。
5.5 子路径部署
站点部署在 https://example.com/admin/ 时:
server {
listen 80;
server_name example.com;
root /data/www;
location /admin/ {
alias /data/www/admin/;
index index.html;
try_files $uri $uri/ /admin/index.html;
}
}
前端构建也要同步设置公共路径(Vite 的 base、webpack 的 publicPath)。两侧路径不一致会导致白屏和大量 404。
六、反向代理
6.1 proxy_pass 结尾斜杠的区别
# A:不带斜杠,原样转发 URI
location /api/ {
proxy_pass http://127.0.0.1:3000;
}
# 请求 /api/user → 后端收到 /api/user
# B:带斜杠,替换掉 location 前缀
location /api/ {
proxy_pass http://127.0.0.1:3000/;
}
# 请求 /api/user → 后端收到 /user
规则:带 / 表示截掉 location 前缀再转发。配置前先确认后端期望的路径形式。
6.2 必要的请求头
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;
缺少这些头的后果:
Host会是内部地址(如api1:3000),后端生成的回调地址、重定向地址不正确。- 后端拿到的客户端 IP 全是 nginx 的地址,访问日志、限流、风控失效。
- 后端无法判断原始协议是 HTTP 还是 HTTPS,可能出现重定向循环。
示例后端会把收到的头回显出来,便于确认:
"upstreamHeaders": {
"host": "localhost",
"x-real-ip": "172.20.0.1",
"x-forwarded-for": "172.20.0.1",
"x-forwarded-proto": "https"
}
6.3 三个超时
proxy_connect_timeout 3s; # 与上游建立连接的超时
proxy_send_timeout 30s; # 发送请求的超时
proxy_read_timeout 30s; # 等待上游响应的超时(最常调整)
后端全部不可用时,访问日志中的上游耗时字段可以说明失败过程:
"GET /api/info HTTP/1.1" 504 160 ... rt=6.009 uct=-, - uht=-, - urt=3.003, 3.006
urt=3.003, 3.006 表示两台上游各自耗时 3 秒(连接超时),总耗时 6 秒后返回 504。这也解释了"后端挂掉后接口要等若干秒才报错"的现象:等待时间由 proxy_connect_timeout × 上游数量 决定。
6.4 故障转移
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
实测(示例工程):停止 api-1 后连续请求,全部由 api-2 返回 200:
第1次 -> api-2
第2次 -> api-2
第3次 -> api-2
第4次 -> api-2
api-1 恢复并超过 fail_timeout 后,请求重新在两台之间分配:
第1次 -> api-2
第2次 -> api-1
第3次 -> api-2
第4次 -> api-1
需要注意语义:proxy_next_upstream 是"换一台上游重试",只对幂等请求安全。如果 POST 请求已被后端处理但响应超时,重试会造成重复提交。生产环境建议 POST 仅允许 error(连接失败)重试,不允许 timeout 重试。
6.5 WebSocket
location /ws/ {
proxy_pass http://app_cluster;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}
三个条件缺一不可:HTTP/1.1、Upgrade 头、Connection: upgrade。缺任一条会连接失败或立即断开。proxy_read_timeout 需要根据心跳间隔适当放大。
6.6 上传大小限制
client_max_body_size 20m; # 默认 1m
默认只有 1MB,超出返回 413。这是"本地能上传、线上上传失败"的常见原因。
七、负载均衡
7.1 upstream 与调度算法
upstream app_cluster {
least_conn;
server api1:3000 max_fails=2 fail_timeout=5s;
server api2:3000 max_fails=2 fail_timeout=5s;
keepalive 16;
}
| 算法 | 写法 | 适用场景 |
|---|---|---|
| 轮询(默认) | 不写 | 后端性能一致 |
| 加权 | server a weight=3; |
机器配置不同 |
| 最少连接 | least_conn; |
请求耗时差异大 |
| IP 哈希 | ip_hash; |
需要会话绑定 |
| 一致性哈希 | hash $request_uri consistent; |
提升缓存命中率 |
7.2 健康检查
开源版 nginx 没有主动健康检查,只有被动摘除:
max_fails=2:连续失败 2 次后标记为不可用。fail_timeout=5s:5 秒后重新尝试。
因此"后端挂掉后需要等待一段时间才摘除"是正常行为。主动健康检查需要 Nginx Plus(商业版)或第三方模块(需重新编译)。
7.3 轮询验证
连续 6 次请求,结果在两台后端间交替:
第1次 -> api-1
第2次 -> api-2
第3次 -> api-1
第4次 -> api-2
第5次 -> api-1
第6次 -> api-2
7.4 会话保持
ip_hash 按客户端 IP 取模分配,但存在明显限制:
- 同一出口 IP 的用户会集中到一台后端。
- 移动网络切换导致 IP 变化时,会话丢失。
- 负载容易不均。
更稳妥的做法是让后端无状态(session 存 Redis),或使用 cookie 粘滞方案(需要第三方模块或在应用层实现)。ip_hash 适合作为临时方案。
7.5 keepalive 长连接
upstream app_cluster {
server api1:3000;
keepalive 16;
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://app_cluster;
}
三行必须同时存在。只配置 keepalive 16 而缺少后两行时,长连接不会生效。
八、HTTPS 与 HTTP/2
8.1 证书来源
| 来源 | 成本 | 适用场景 |
|---|---|---|
| Let's Encrypt(certbot) | 免费,90 天自动续期 | 个人项目、小团队 |
| 云厂商证书 | 免费 DV 或付费 | 已备案的国内站点 |
| 自签证书 | 免费 | 本地开发与测试 |
示例工程使用自签证书(conf/certs/dev.crt、dev.key),仅用于本地启动 HTTPS 环境,不能用于线上。
8.2 配置
server {
listen 80;
server_name example.com;
# Let's Encrypt 续期验证路径
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on; # nginx ≥ 1.25.1
server_name example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# 站点配置
}
旧写法 listen 443 ssl http2; 在 1.25.1 之后会提示 deprecated,应改为独立的 http2 on;。
8.3 验证 HTTP/2
部分 curl 构建未包含 HTTP/2 支持(curl: option --http2: the installed libcurl version does not support this),可以改用 Node 的 http2 模块:
node -e "
const http2 = require('http2');
const c = http2.connect('https://localhost:8443', { rejectUnauthorized: false });
c.on('connect', () => { console.log('ALPN =', c.alpnProtocol); c.close(); });
"
# ALPN = h2
访问日志中会记录为 HTTP/2.0:
172.20.0.1 - - [...] "GET / HTTP/2.0" 200 1075 ...
8.4 HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
只有在确认全站 HTTPS 可用后再启用。启用后浏览器会在 max-age 期间强制使用 HTTPS,配置失误时恢复成本较高。
8.5 证书续期
sudo certbot certonly --webroot -w /var/www/certbot -d example.com
sudo nginx -s reload
certbot 的定时任务会自动续期,但续期后需要 reload,否则 nginx 仍在使用旧证书,表现为"证书已续期但仍然过期"。
九、缓存
9.1 浏览器缓存
# 带 hash 的静态资源
add_header Cache-Control "public, max-age=31536000, immutable" always;
# HTML 入口
add_header Cache-Control "no-cache" always;
no-cache 的含义是"使用前回源确认",不是"不缓存"。真正不缓存是 no-store。
常见资源配置:
| 资源 | 策略 | 原因 |
|---|---|---|
index.html |
no-cache |
它更新后整个应用才会更新 |
| 带 hash 的 JS/CSS | max-age=31536000, immutable |
内容变化会改变文件名 |
| 不带 hash 的图片 | max-age=86400 |
折中 |
| 接口 GET | 不缓存或很短 | 保证数据新鲜度 |
9.2 代理缓存
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
max_size=1g inactive=60m use_temp_path=off;
location /api/ {
proxy_cache api_cache;
proxy_cache_valid 200 302 60s;
proxy_cache_valid 404 10s;
proxy_cache_key $scheme$request_method$host$request_uri;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
}
X-Cache-Status 会返回 HIT、MISS、EXPIRED、UPDATING,没有这个响应头很难判断缓存是否生效。
带用户身份的接口不能缓存,否则会出现越权读取他人数据的问题。建议 /api/ 默认不缓存,只对明确无状态的接口单独开启。
9.3 三层缓存的关系
浏览器缓存(Cache-Control) → nginx 代理缓存(proxy_cache) → 后端与数据库
排查缓存问题时,先确定当前命中哪一层。有时修改 Cache-Control 无效,是因为 CDN 在更外层缓存。
十、限流与基础安全
10.1 limit_req
# http 块:定义限流规则
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
# location 块:应用规则
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
limit_req_status 429;
}
rate=5r/s:平均每 0.2 秒放行一个请求。burst=10:允许瞬时超出的排队数量。nodelay:突发请求立即处理,不排队等待(不加会显著增加延迟)。limit_req_status 429:默认拒绝返回 503,改为 429 语义更准确。
实测(示例工程,30 次连续请求):
200 -> 17 次
503 -> 13 次
结果说明限流不是"超过 5 个就全部拒绝",而是按速率补充加突发额度。
10.2 limit_conn
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
location /download/ {
limit_conn conn_limit 2; # 同一 IP 最多 2 个并发连接
}
10.3 基础加固
server_tokens off; # 不暴露版本号
location ~ /\.(?!well-known) { # 禁止访问 .git、.env 等
deny all;
return 404;
}
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
return 405;
}
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
if ($request_method ...) 这种简单判断是安全的;复杂条件判断见第十一节。
十一、变量、map、rewrite 与 if
11.1 常用内置变量
| 变量 | 含义 |
|---|---|
$host / $http_host |
请求域名,$host 不含端口,更安全 |
$request_uri |
原始完整 URI(含参数) |
$uri |
当前 URI(经过 rewrite 后可能变化) |
$args / $query_string |
查询串 |
$remote_addr |
客户端 IP(有代理时是代理 IP) |
$http_x_forwarded_for |
代理链上的客户端 IP 列表 |
$scheme |
http 或 https |
$request_method |
请求方法 |
$status |
响应状态码 |
$request_time |
请求总耗时 |
$upstream_response_time |
上游响应耗时 |
$upstream_addr |
实际处理请求的上游地址 |
$upstream_cache_status |
缓存命中状态 |
11.2 map
# 按 UA 分流
map $http_user_agent $site_root {
default /data/www/pc;
~*Mobile /data/www/mobile;
}
# 按请求头开关功能
map $http_x_debug $enable_debug {
default 0;
"1" 1;
}
server {
root $site_root;
}
map 在 http 块中定义一次即可处处使用,不产生 if 的副作用,是条件分支的首选方式。
11.3 rewrite 与 return
# 301 永久跳转
rewrite ^/old/(.*)$ /new/$1 permanent;
# 整站跳转优先使用 return
return 301 https://new.example.com$request_uri;
区别:
rewrite会重新执行 location 匹配,条件复杂时可能造成循环。return直接返回,不重新匹配。
能用 return 就不使用 rewrite。
11.4 if 的使用限制
nginx 官方文档标注 "if is evil",原因是:
if在 location 中会创建隐式嵌套 location,try_files可能失效。if块中的add_header不一定生效,继承规则被破坏。
建议 if 只用于 return 或 rewrite,其余条件分支使用 map 或拆分成独立 location。
11.5 sub_filter
location / {
sub_filter '</head>' '<script src="/runtime-config.js"></script></head>';
sub_filter_once on;
sub_filter_types text/html;
}
常见用途是同一份前端产物注入不同环境的运行时配置。注意 sub_filter 处理的是未压缩内容,与 gzip 同时使用时需确认生效顺序。
11.6 动态 upstream
resolver 127.0.0.11 valid=10s; # Docker 内置 DNS;线上可改为内网 DNS
location ~ ^/tenant/(?<tenant>[a-z0-9-]+)/ {
set $backend "http://${tenant}.internal:3000";
proxy_pass $backend;
}
使用变量后 upstream 的 keepalive 失效,且必须配置 resolver,否则启动时会报 no resolver defined。
十二、stream:四层代理
HTTP 层无法转发 MySQL、Redis、MQ 等 TCP 协议,stream 块负责四层转发:
stream {
upstream mysql_backend {
server 10.0.0.11:3306;
server 10.0.0.12:3306;
}
server {
listen 3306;
proxy_pass mysql_backend;
proxy_connect_timeout 3s;
proxy_timeout 300s;
}
}
注意事项:
stream与http平级,不能写在http内部。- 超时使用
proxy_timeout,四层没有proxy_read_timeout。 - 官方 Docker 镜像包含 stream 模块;Debian 的
nginx-light不包含,需安装nginx-full。
十三、性能相关配置
worker_processes auto; # 通常等于 CPU 核数
worker_connections 1024; # 每个 worker 的最大连接数
worker_rlimit_nofile 65535; # 每个 worker 可打开的文件句柄数
sendfile on; # 零拷贝发送静态文件
tcp_nopush on; # 与 sendfile 配合,攒满数据包再发送
tcp_nodelay on; # keepalive 连接下禁用 Nagle
keepalive_timeout 65;
client_header_timeout 15s;
client_body_timeout 15s;
几点说明:
worker_connections是每个 worker 的上限,理论并发约为worker_processes × worker_connections。作为反向代理时,一个客户端请求会占用两个连接(客户端与上游),实际并发要减半。tcp_nopush作用于sendfile的响应;tcp_nodelay作用于 keepalive 连接,二者并不冲突。proxy_buffering on是默认值,会先将上游响应缓冲再发送,可释放后端连接。流式接口(SSE、大文件下载)需关闭,否则会出现数据不输出的情况。- 压测时不要只看 QPS,还要看
$request_time分布、$upstream_response_time,以及error.log中是否有worker_connections are not enough。
压测示例:
wrk -t4 -c200 -d30s --latency https://example.com/
日志切割
nginx 不会自动切割日志,通常借助 logrotate:
sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.1
sudo nginx -s reopen
容器环境中可以直接通过 docker compose logs 查看,或把日志目录挂载出来自行管理。
十四、排错
14.1 排查顺序
nginx -t检查语法与文件是否存在。curl -I查看状态码与响应头,先区分是 nginx 的问题还是后端的问题。- 查看
error.log最后若干行。 - 查看
access.log中的rt、urt、upstream_addr。 - 确认配置确实生效:是否 reload、是否修改了被 include 的文件(可用
nginx -T确认)。
14.2 常见状态码
| 状态码 | 常见原因 | 处理方向 |
|---|---|---|
| 400 | 请求头或请求行不合法 | 检查 client_header_* 配置与客户端行为 |
| 403 | 目录权限不足、缺少 index、deny all |
检查文件权限与 autoindex |
| 404 | 路径错误、root/alias 用错、try_files 未配置 |
对照请求 URI 与磁盘路径 |
| 413 | 上传超过 client_max_body_size |
调大该值 |
| 499 | 客户端主动断开(nginx 特有状态码),通常因后端响应过慢 | 优化后端耗时 |
| 502 | 上游连接失败或返回非法响应 | 检查后端进程、端口、容器网络 |
| 504 | 上游响应超时 | 查看 urt,调整 proxy_read_timeout 或优化后端 |
14.3 502 与 504 的区别
- 502 Bad Gateway:连接建立后,上游返回的内容不合法(进程崩溃、端口未监听、返回非 HTTP 内容)。
- 504 Gateway Timeout:连接已建立并发出请求,但等待响应超时。
在同一示例中停止全部后端,返回的是 504,日志为:
"GET /api/info HTTP/1.1" 504 160 ... rt=6.009 uct=- uht=- urt=3.003, 3.006
原因是容器停止后连接表现为超时而非拒绝。因此"后端不可用"既可能是 502 也可能是 504,需要结合 error.log 与 urt 判断,不能只凭状态码下结论。
14.4 配置未生效的常见原因
- 没有执行
nginx -s reload。 - 修改的文件没有被
include。 - 浏览器或 CDN 缓存了旧响应。
- 上游存在多个实例,只重启了其中一个。
nginx -T 可以输出 include 展开后的完整配置,用于确认最终生效内容。
十五、上线检查清单
-
nginx -t通过,nginx -T中能看到预期的 server 与 location -
index.html为no-cache,静态资源为长缓存 - 单页应用深层路由刷新正常(
try_files生效) - gzip 生效(响应头包含
Content-Encoding: gzip) - HTTPS 证书链完整,
curl -I https://...无证书错误 - HTTP 到 HTTPS 的 301 跳转正常
-
client_max_body_size满足业务上传需求 - 反向代理已透传
Host与X-Forwarded-* - 上游配置了
max_fails/fail_timeout,POST 请求未开启 timeout 重试 - 访问日志包含
$request_time与$upstream_response_time,且已配置切割 -
server_tokens off - 修改前已备份旧配置
十六、FAQ
Q1:root 和 alias 怎么选?
优先 root。仅当 URL 前缀与磁盘路径不一致时使用 alias,并保证结尾斜杠对齐。
Q2:为什么 location ~ \.js$ 会抢走 /api/xxx.js?
正则优先于普通前缀。将该接口 location 改为 location ^~ /api/ 即可避免。
Q3:proxy_pass 带不带斜杠?
带斜杠会截掉 location 前缀;不带则原样转发。取决于后端期望的路径。
Q4:后端拿到的客户端 IP 为什么是内网地址?
未配置 X-Real-IP / X-Forwarded-For,后端只能看到 nginx 或容器网关的地址。
Q5:if 能不能用?
只用于 return / rewrite 是安全的;用于修改 root、proxy_pass 等会出问题,应改用 map。
Q6:reload 会断开连接吗?
不会。旧 worker 处理完当前请求后退出,属于平滑重启。但配置有语法错误时 reload 会失败并保持旧配置,因此应先执行 nginx -t。
Q7:index.html 修改后用户仍是旧页面?
HTML 被缓存。检查是否在 server 级写了 expires,导致 HTML 也被长缓存。
Q8:静态资源报 404,但文件确实存在?
多为 root 与 alias 混用,或 location 前缀被拼进了路径。对照请求 URI 与磁盘路径即可。
Q9:limit_req 配置后没有效果?
检查 limit_req_zone 是否定义(在 http 块)以及 location 是否引用;同时确认 rate 单位写法为 5r/s。
Q10:容器内 nginx 转发 localhost:3000 为什么不通?
容器内的 localhost 指向容器自身。应使用 host.docker.internal(Windows/macOS)或 compose 服务名(如 api1:3000)。
十七、版本与镜像选型
| 版本 | 变化点 | 影响 |
|---|---|---|
| 1.25.1+ | listen ... http2 改为 http2 on; |
旧写法提示 deprecated |
| 1.25+ | http2 不再强制与 ssl 绑定 |
支持明文 HTTP/2 |
| 1.19+ | 支持 ssl_early_data |
可实现 0-RTT,需防重放 |
| 1.9.5+ | stream 模块内置 |
四层代理无需额外编译 |
镜像选择:
nginx:stable-alpine:体积小,常用于生产;注意 alpine 使用 musl,部分第三方模块不兼容。nginx:stable:Debian 基础,兼容性好,体积较大。- 避免使用
nginx:latest,版本会漂移。
附录 A:示例工程
目录结构:
nginx/
├─ docker-compose.yml # nginx + 两个 Node 后端(api1 / api2)
├─ conf/
│ ├─ nginx.conf # 本文示例的合集
│ └─ certs/dev.crt|dev.key # 自签证书,仅本地使用
├─ html/ # 前端示例页面
├─ backend/server.js # 零依赖 Node 后端,回显请求头与实例名
└─ logs/ # 挂载的访问日志
启动:
cd nginx
docker compose up -d
docker compose exec nginx nginx -t
# 浏览器访问 https://localhost:8443(自签证书,需手动信任)
# Windows 也可执行:start https://localhost:8443
复现文中结论的命令(Git Bash / WSL / Linux 可直接使用):
# 1) HTTP 跳转
curl -sI http://localhost:8080/
# 2) 单页应用回退
curl -k -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/some/spa/route
# 3) gzip 与缓存头
curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js
# 4) 负载均衡(多次请求观察 instance 字段)
for i in $(seq 1 6); do curl -k -s https://localhost:8443/api/info | grep instance; done
# 5) 限流
for i in $(seq 1 30); do curl -k -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/api/info; done | sort | uniq -c
# 6) 故障转移
docker stop nginx-demo-api1
curl -k -s https://localhost:8443/api/info | grep instance
# 7) 状态页
curl -k -s https://localhost:8443/nginx-status
# 8) 日志
tail -f logs/access.log
停止并清理:
docker compose down
附录 B:完整配置模板
以下配置对应 conf/nginx.conf 的核心部分,可直接作为新项目的起点。
worker_processes auto; # 跟随 CPU 核数
error_log /var/log/nginx/error.log warn;
events {
worker_connections 1024; # 单 worker 最大连接数
}
http {
include /etc/nginx/mime.types; # 缺少它,JS/CSS 会被当作 text/plain
default_type application/octet-stream;
log_format main '$remote_addr [$time_local] "$request" $status '
'$body_bytes_sent rt=$request_time urt=$upstream_response_time';
gzip on;
gzip_types text/plain text/css application/javascript application/json;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
upstream app_cluster {
least_conn;
server api1:3000 max_fails=2 fail_timeout=5s;
server api2:3000 max_fails=2 fail_timeout=5s;
keepalive 16;
}
server {
listen 80;
return 301 https://$host$request_uri; # 全站 HTTPS
}
server {
listen 443 ssl;
http2 on;
server_name localhost;
ssl_certificate /etc/nginx/certs/dev.crt;
ssl_certificate_key /etc/nginx/certs/dev.key;
root /usr/share/nginx/html;
index index.html;
# 静态资源:长缓存;缺文件直接 404,不回退到 index.html
location ~* \.(?:js|css|png|jpe?g|svg|ico|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
# 入口 HTML:每次回源确认
location = /index.html {
add_header Cache-Control "no-cache" always;
}
# 单页应用路由
location / {
try_files $uri $uri/ /index.html;
}
# 接口反向代理
location ^~ /api/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass http://app_cluster/; # 结尾 / 会截掉 /api/
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 3s;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_502 http_503;
}
}
}
这份配置覆盖静态站点、单页应用回退、接口代理、负载均衡、HTTPS、压缩、限流与缓存,可作为实际项目的初始模板,再按需增删。