第 16 章 · Nginx
一句话:高性能的 Web 服务器和反向代理,一台机器上托管静态网站、把请求按路径转给后端应用、在前面统一处理 HTTPS。 用它还是用别的:Linux 服务器上给应用挡在前面、配 HTTPS、托管前端打包文件,用它;只想一行配置自动申请证书、规则简单的小站,用 Caddy(第 17 章);本机开发直接访问应用的端口,不必加 Nginx。 核对版本:Nginx 1.30(stable,镜像
nginx:stable-alpine),2026-10
16.1 安装与版本
Section titled “16.1 安装与版本”本机练习用随书 compose 的 web 组,Compose 的用法详见第 13 章。在随书的 compose/ 目录下启动:
docker compose --profile web up -d这一组同时起 Nginx 和 Caddy。Nginx 在宿主机的 8080 端口,浏览器打开 http://localhost:8080 看到“Web 服务器已经跑起来了”就对了。它读的配置是 compose/nginx/default.conf,网页目录是 compose/html/,两者都是只读挂载,在宿主机上改文件即可。
Linux 服务器(Debian/Ubuntu)用系统包,装完自动启动并设为开机自启:
sudo apt updatesudo apt install nginxWindows 版从 Nginx 官网下载页下载 zip(网址见 16.7),解压后在目录里运行 start nginx。官方说明它是测试版:多个工作进程里只有一个真正干活,不要指望性能,只适合本机试配置,正式部署放 Linux 上。
查版本,输出 nginx version: nginx/1.30.x;-V 还会列出编译参数,确认带了哪些模块:
nginx -vnginx -Vdocker compose exec nginx nginx -v16.2 五分钟概念
Section titled “16.2 五分钟概念”- 主进程与工作进程(master / worker):主进程读配置、管理工作进程,不处理请求;工作进程才处理请求。重载配置时,主进程先检查新配置,没问题就起新工作进程、让旧的处理完手头请求再退出,所以 reload 不会断开正在进行的连接;新配置有错就继续用旧的。
- 配置的层级(context):配置文件是一层套一层的块。最外层叫 main,放
worker_processes这类全局项;events管连接;http里放所有网站共用的设置;http里一个server是一个网站;server里一个location处理一类路径。内层没写的设置从外层继承,写了就覆盖。每条指令只能写在文档规定的层级里,写错层nginx -t会报错。 - 虚拟主机(virtual server):一台 Nginx 可以托管很多网站。请求进来,先按
listen的端口挑出候选的server,再拿请求头里的域名(Host)和各自的server_name比对;都对不上就交给这个端口的默认 server,即标了default_server的那个,没标就是配置里第一个。 - location 匹配顺序:
location = /x是精确匹配,命中立刻用它;否则先在普通前缀里找最长匹配的那个记下来,它如果带^~就直接用;再按书写顺序试正则(~区分大小写,~*不区分),第一个命中的胜出;正则都不中,才用刚才记下的最长前缀。所以一条宽泛的正则可以抢走本该由前缀处理的请求。 - 反向代理(reverse proxy):浏览器只连 Nginx,Nginx 用
proxy_pass把请求转给后面的应用,再把响应交回浏览器。应用不用直接暴露在公网,HTTPS、压缩、上传大小限制都在 Nginx 这一层统一处理。 - root 与 alias:
root把完整的请求路径接在目录后面,root /data/w3;下请求/i/top.gif读/data/w3/i/top.gif;alias用目录替换掉 location 匹配的那一段,location /i/ { alias /data/w3/images/; }下同一请求读/data/w3/images/top.gif。目录名和路径相同时用root。
下图是一个典型站点的请求路径:浏览器只和 Nginx 打交道,Nginx 先选出网站,再按路径把请求分给三个去处。/ 读磁盘上的静态文件,/api/ 转给本机 3000 端口的应用,/ws/ 转给另一个服务并带上 WebSocket 需要的 Upgrade 头。
16.3 常用命令
Section titled “16.3 常用命令”Linux 包安装的 Nginx 要 sudo;随书 compose 里的在命令前加 docker compose exec nginx,不用 sudo。配置写法一组的内容写进配置文件,改完都要先 nginx -t 再重载。
| 任务 | 命令 | 说明 |
|---|---|---|
| 检查与重载 | ||
| 检查配置 | sudo nginx -t |
看到 syntax is ok 和 test is successful 才能重载;出错会给出文件名和行号 |
| 检查并打印全部配置 | sudo nginx -T |
include 进来的文件也一起打印,确认改动到底有没有被读到 |
| 重载配置 | sudo nginx -s reload |
不断开现有连接;新配置有错时继续用旧配置 |
| 优雅停止 | sudo nginx -s quit |
处理完手头请求再退出;-s stop 是立即退出 |
| systemd(Linux 包安装) | ||
| 看运行状态 | systemctl status nginx |
active (running) 是在跑;启动失败时下面几行就是原因 |
| 重载 | sudo systemctl reload nginx |
效果同 nginx -s reload |
| 重启 | sudo systemctl restart nginx |
会断开现有连接,平时用 reload |
| 开机自启 | sudo systemctl enable nginx |
apt 安装时已经启用 |
| 随书 compose | ||
| 只起 Nginx | docker compose up -d nginx |
指名服务不受组限制,Caddy 不会跟着起 |
| 容器里检查配置 | docker compose exec nginx nginx -t |
检查的是容器里挂进去的那份 |
| 容器里重载 | docker compose exec nginx nginx -s reload |
改了 compose/nginx/default.conf 后用 |
| 看日志 | docker compose logs -f nginx |
镜像把访问日志和错误日志都输出到这里 |
| 日志 | ||
| 跟踪访问日志 | sudo tail -f /var/log/nginx/access.log |
一个请求一行:来源 IP、时间、请求行、状态码、字节数 |
| 跟踪错误日志 | sudo tail -f /var/log/nginx/error.log |
502、404 的具体原因写在这里 |
| 找出 5xx 请求 | sudo grep '" 5[0-9][0-9] ' /var/log/nginx/access.log |
默认日志格式里状态码紧跟在请求行的引号后面 |
| 配置写法 | ||
| 静态站点 | root /var/www/site; index index.html; |
写在 server 里;请求 /a.css 读 /var/www/site/a.css |
| 反向代理 | location /api/ { proxy_pass http://127.0.0.1:3000; } |
地址后不带路径,/api/x 原样转发;写成 3000/ 会去掉 /api/ 前缀 |
| 转发域名 | proxy_set_header Host $host; |
后端拿到浏览器访问的域名;不写时是 proxy_pass 里的地址 |
| 转发真实 IP | proxy_set_header X-Real-IP $remote_addr; |
不写的话后端日志里的 IP 全是 Nginx 自己 |
| 转发代理链 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
在已有的 X-Forwarded-For 后追加客户端 IP |
| 转发协议 | proxy_set_header X-Forwarded-Proto $scheme; |
后端知道原请求是 https,生成的链接和跳转不会变成 http |
| WebSocket | proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; |
三行和 proxy_pass 写在同一个 location;1.29.7 起默认就是 1.1,第一行写上也无妨,旧版本少了它连不上 |
| 压缩 | gzip on; gzip_types text/css application/javascript application/json; |
默认关闭;text/html 开了就压,其他类型要列在 gzip_types 里 |
| 上传大小 | client_max_body_size 50m; |
默认 1m,超出返回 413;0 是不限 |
| 跳转 HTTPS | return 301 https://$host$request_uri; |
写在 listen 80 的 server 里,路径和参数原样带上 |
| 单页应用 | location / { try_files $uri $uri/ /index.html; } |
文件不存在就返回 index.html,前端路由刷新不再 404 |
16.4 典型工作流
Section titled “16.4 典型工作流”用随书 compose 起一个静态站
Section titled “用随书 compose 起一个静态站”随书 compose/nginx/default.conf 只有一个 server 块:listen 80、root /usr/share/nginx/html、index index.html。compose/html/ 挂在这个目录上,所以改网页不用重载:
- 进入目录:
cd compose。 - 起 web 组:
docker compose --profile web up -d。 - 确认 PORTS 列是
127.0.0.1:8080->80/tcp:docker compose ps nginx。 - 取首页,返回“Web 服务器已经跑起来了”那段 HTML:
curl http://localhost:8080。 - 用编辑器改网页,把那句话换成别的:
code html/index.html。 - 浏览器刷新
http://localhost:8080,新内容立刻出现;看这次请求的访问日志:docker compose logs --tail 5 nginx。 - 在
html/下新建about.html,访问http://localhost:8080/about.html;看容器里实际生效的配置:docker compose exec nginx nginx -T。 - 用完停掉,Caddy 一起停:
docker compose --profile web down。
给本机 3000 端口的应用加反向代理
Section titled “给本机 3000 端口的应用加反向代理”本机有一个应用在 3000 端口,想通过 http://localhost:8080/api/ 访问它。下面用 Python 自带的文件服务器充当应用:
- 在
compose/目录起 Nginx,上一个工作流已经down掉的话要重新起:docker compose up -d nginx。 - 在另一个终端里起应用,它会列出当前目录的文件:
python -m http.server 3000。 - 确认应用本身能访问:
curl http://localhost:3000/。 - 编辑
compose/nginx/default.conf,在server块里加上下面的location /api/:code nginx/default.conf。 - 检查配置:
docker compose exec nginx nginx -t。 - 重载:
docker compose exec nginx nginx -s reload。 - 通过 Nginx 访问,看到和第 3 步一样的文件列表就通了:
curl http://localhost:8080/api/。 - 返回
502 Bad Gateway时看错误日志里连的是哪个地址:docker compose logs --tail 20 nginx。
server { listen 80; root /usr/share/nginx/html; # 静态文件目录 index index.html;
location /api/ { proxy_pass http://host.docker.internal:3000/; # 末尾的 / 去掉 /api/ 前缀 proxy_set_header Host $host; }}Nginx 在容器里,127.0.0.1 是容器自己,那里没有 3000 端口,写成 proxy_pass http://127.0.0.1:3000/ 必然 502。host.docker.internal 是 Docker Desktop 提供的宿主机地址。Linux 上的 Docker Engine 默认没有这个名字,nginx -t 会报 host not found in upstream "host.docker.internal",要在 compose.yaml 的 nginx 服务下加 extra_hosts: ["host.docker.internal:host-gateway"],应用也要监听 0.0.0.0 而不是 127.0.0.1。Nginx 直接装在服务器上、应用在同一台机器时,才写 127.0.0.1:3000。
用 Certbot 给站点配 HTTPS
Section titled “用 Certbot 给站点配 HTTPS”在 Linux 服务器上,前提:域名已解析到这台服务器,80 和 443 端口对公网开放,Nginx 里已有 server_name 写着这个域名的 server。Certbot 会申请 Let’s Encrypt 的免费证书并改好 Nginx 配置:
- 在自己电脑上确认域名指向这台服务器,响应头里有
Server: nginx:curl -I http://example.com。 - 服务器开了 ufw 防火墙的,放行 80 和 443:
sudo ufw allow 'Nginx Full'。 - 用 snap 装 Certbot:
sudo snap install --classic certbot。 - 让
certbot命令能直接用:sudo ln -s /snap/bin/certbot /usr/local/bin/certbot。 - 申请证书并自动改配置,按提示填邮箱、同意条款:
sudo certbot --nginx -d example.com -d www.example.com。 - 确认 HTTPS 已生效,能拿到响应头、没有证书报错:
curl -I https://example.com。 - 确认 HTTP 被跳到 HTTPS,返回
301和Location: https://example.com/:curl -I http://example.com。 - 演练续期,不会真的换证书:
sudo certbot renew --dry-run。 - 确认自动续期的定时器在:
systemctl list-timers | grep certbot。
Certbot 往 server 块里加了 listen 443 ssl 和指向 /etc/letsencrypt/live/example.com/ 的证书路径,并加了 80 端口跳转 HTTPS 的规则。证书有效期 90 天,定时器定期检查,剩不到 30 天时自动续期。本机的随书 compose 没有公网域名,申请不了证书。
16.5 配置要点
Section titled “16.5 配置要点”配置文件的位置:
| 配置项 | Linux(apt) | Windows | Docker 镜像内 |
|---|---|---|---|
| 主配置 | /etc/nginx/nginx.conf |
conf\nginx.conf |
/etc/nginx/nginx.conf |
| 网站配置 | /etc/nginx/conf.d/*.conf;Ubuntu 默认站点在 /etc/nginx/sites-enabled/default |
直接写在 conf\nginx.conf 的 http 块里 |
/etc/nginx/conf.d/default.conf(随书挂载的就是它) |
| 日志 | /var/log/nginx/access.log、error.log |
logs\ |
输出到容器日志,用 docker compose logs nginx 看 |
| 网页目录 | Ubuntu 默认站点 /var/www/html |
html\ |
/usr/share/nginx/html |
网站配置一个网站一个文件,放进 conf.d/ 并以 .conf 结尾,主配置的 http 块用 include 把它们读进来。Ubuntu 的习惯是写在 sites-available/ 里,再在 sites-enabled/ 建一个链接启用;不想要默认站点就删掉 sites-enabled/default,否则它会占着 80 端口的默认 server。
值得改的项:
| 配置项 | 建议值 | 效果 |
|---|---|---|
worker_processes |
auto |
按 CPU 核数起工作进程。指令默认 1;apt 包和官方镜像的 nginx.conf 已写成 auto,手写主配置时别漏。写在 main 层 |
server_tokens |
off |
错误页和 Server 响应头不再显示版本号,扫描器少一条线索 |
client_max_body_size |
按业务定,如 20m |
默认 1m,传一张手机照片就可能 413 |
log_format |
在默认格式后加 $request_time $upstream_response_time |
一个慢请求能分清是 Nginx 慢还是后端慢;写在 http 层,再用 access_log <文件> <格式名>; 启用 |
proxy_read_timeout |
默认 60s,导出报表等慢接口调到 300s |
后端 60 秒没回数据,Nginx 断开并返回 504 Gateway Time-out |
proxy_connect_timeout |
5s |
默认 60 秒;后端主机不可达(宕机、防火墙丢包)时 5 秒就报错。后端进程没起是连接被拒,立刻 502,不受它影响 |
keepalive_timeout |
默认 75s,一般不用改 |
浏览器连接空闲多久后断开 |
Linux 服务器上一个最小的反向代理网站,保存为 /etc/nginx/conf.d/example.com.conf,nginx -t 通过后 systemctl reload nginx:
server { listen 80; # 监听 80 端口 server_name example.com; # 请求的域名是它才进这个 server server_tokens off; # 不在响应头里暴露版本号 client_max_body_size 20m; # 上传上限,默认 1m access_log /var/log/nginx/example.access.log; # 每个网站单独一份访问日志
location / { proxy_pass http://127.0.0.1:3000; # 转给本机 3000 端口的应用,路径原样转发 proxy_set_header Host $host; # 后端看到浏览器访问的域名 proxy_set_header X-Real-IP $remote_addr; # 客户端真实 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 经过的代理链 proxy_set_header X-Forwarded-Proto $scheme; # 原请求是 http 还是 https proxy_read_timeout 60s; # 后端多久不回数据就返回 504 }}配 HTTPS 时不用自己改这个文件,16.4 的 Certbot 会往里加 443 和证书。要手写 HTTPS 配置,用 16.7 里的 Mozilla SSL 配置生成器选好 Nginx 版本,照着生成的 ssl_protocols 和 ssl_ciphers 写。
16.6 注意事项
Section titled “16.6 注意事项”- 浏览器显示
502 Bad Gateway,错误日志有connect() failed (111: Connection refused)。 原因:后端没起、端口不对,或容器里的 Nginx 连127.0.0.1,那是容器自己。处理:curl直连后端确认它在;容器里改连host.docker.internal或服务名。 - 上传文件失败,返回
413 Request Entity Too Large。 原因:请求体超过client_max_body_size,默认只有 1 MB。处理:在server或对应location里加client_max_body_size 50m;,nginx -t后 reload。 - 文件明明在,访问却 404,错误日志里
open()的路径多了或少了一段。 原因:location /img/里写root /data/img会去找/data/img/img/;alias少了末尾斜杠会拼成/data/imga.png。处理:目录名和路径相同用root /data;用alias时两边都以/结尾。 - 改了配置刷新页面没变化。 原因:没有 reload,或 reload 时新配置有错,Nginx 继续用旧配置。处理:
nginx -t看报错,改好再 reload;随书 compose 里仍不变就docker compose restart nginx。 - 启动失败,
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)。 原因:80 端口已被另一个 Nginx、Apache 或其他程序占着。处理:sudo ss -ltnp | grep :80找出进程停掉,或把listen改到别的端口。 - WebSocket 连不上,或连上后 60 秒左右自动断开。 原因:没转发
Upgrade和Connection头,Nginx 当普通请求处理;60 秒断开是proxy_read_timeout到期。处理:location 里加 16.3 的 WebSocket 三行;长连接调大proxy_read_timeout,或让应用定时发 ping。 - 浏览器报
NET::ERR_CERT_DATE_INVALID,证书过期了。 原因:Certbot 自动续期没跑成功,常见是 80 端口被防火墙关了或 DNS 改了。处理:sudo certbot renew --dry-run看报错;续上后sudo systemctl reload nginx。 - 后端生成的链接和跳转都是
http://或127.0.0.1:3000。 原因:没转发Host和X-Forwarded-Proto,后端以为请求来自proxy_pass里的地址和 http。处理:加上 16.3 的四行proxy_set_header,后端框架打开信任代理头的设置。
16.7 深入阅读
Section titled “16.7 深入阅读”- Nginx 文档:全部模块和指令的索引,查一条指令的语法、默认值和能写在哪一层。
- 初学者指南:启动停止重载、配置文件结构、静态站点和简单代理,第一次配置先看这篇。
- ngx_http_proxy_module:proxy_pass 带不带路径的区别、proxy_set_header 的默认值、各种超时和缓冲。
- Nginx 下载页:主线版、稳定版的源码包和 Windows zip,Windows 上本机试配置从这里下载。
- Certbot 指引(Nginx):选好服务器系统后给出安装 Certbot、申请证书、测试自动续期的逐条命令。
- Mozilla SSL 配置生成器:选 Nginx 和版本,生成 ssl_protocols、ssl_ciphers 等 HTTPS 配置,手写 443 时照抄。