跳转到内容

第 16 章 · Nginx

一句话:高性能的 Web 服务器和反向代理,一台机器上托管静态网站、把请求按路径转给后端应用、在前面统一处理 HTTPS。 用它还是用别的:Linux 服务器上给应用挡在前面、配 HTTPS、托管前端打包文件,用它;只想一行配置自动申请证书、规则简单的小站,用 Caddy(第 17 章);本机开发直接访问应用的端口,不必加 Nginx。 核对版本:Nginx 1.30(stable,镜像 nginx:stable-alpine),2026-10

本机练习用随书 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 update
sudo apt install nginx

Windows 版从 Nginx 官网下载页下载 zip(网址见 16.7),解压后在目录里运行 start nginx。官方说明它是测试版:多个工作进程里只有一个真正干活,不要指望性能,只适合本机试配置,正式部署放 Linux 上。

查版本,输出 nginx version: nginx/1.30.x;-V 还会列出编译参数,确认带了哪些模块:

终端窗口
nginx -v
nginx -V
docker compose exec nginx nginx -v
  • 主进程与工作进程(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 头。

Nginx 先按 listen 和 server_name 选网站,再按 location 把请求分给静态目录、3000 端口的应用和 4000 端口的另一个服务

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

随书 compose/nginx/default.conf 只有一个 server 块:listen 80、root /usr/share/nginx/html、index index.html。compose/html/ 挂在这个目录上,所以改网页不用重载:

  1. 进入目录:cd compose。
  2. 起 web 组:docker compose --profile web up -d。
  3. 确认 PORTS 列是 127.0.0.1:8080->80/tcp:docker compose ps nginx。
  4. 取首页,返回“Web 服务器已经跑起来了”那段 HTML:curl http://localhost:8080。
  5. 用编辑器改网页,把那句话换成别的:code html/index.html。
  6. 浏览器刷新 http://localhost:8080,新内容立刻出现;看这次请求的访问日志:docker compose logs --tail 5 nginx。
  7. 在 html/ 下新建 about.html,访问 http://localhost:8080/about.html;看容器里实际生效的配置:docker compose exec nginx nginx -T。
  8. 用完停掉,Caddy 一起停:docker compose --profile web down。

给本机 3000 端口的应用加反向代理

Section titled “给本机 3000 端口的应用加反向代理”

本机有一个应用在 3000 端口,想通过 http://localhost:8080/api/ 访问它。下面用 Python 自带的文件服务器充当应用:

  1. 在 compose/ 目录起 Nginx,上一个工作流已经 down 掉的话要重新起:docker compose up -d nginx。
  2. 在另一个终端里起应用,它会列出当前目录的文件:python -m http.server 3000。
  3. 确认应用本身能访问:curl http://localhost:3000/。
  4. 编辑 compose/nginx/default.conf,在 server 块里加上下面的 location /api/:code nginx/default.conf。
  5. 检查配置:docker compose exec nginx nginx -t。
  6. 重载:docker compose exec nginx nginx -s reload。
  7. 通过 Nginx 访问,看到和第 3 步一样的文件列表就通了:curl http://localhost:8080/api/。
  8. 返回 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。

在 Linux 服务器上,前提:域名已解析到这台服务器,80 和 443 端口对公网开放,Nginx 里已有 server_name 写着这个域名的 server。Certbot 会申请 Let’s Encrypt 的免费证书并改好 Nginx 配置:

  1. 在自己电脑上确认域名指向这台服务器,响应头里有 Server: nginx:curl -I http://example.com。
  2. 服务器开了 ufw 防火墙的,放行 80 和 443:sudo ufw allow 'Nginx Full'。
  3. 用 snap 装 Certbot:sudo snap install --classic certbot。
  4. 让 certbot 命令能直接用:sudo ln -s /snap/bin/certbot /usr/local/bin/certbot。
  5. 申请证书并自动改配置,按提示填邮箱、同意条款:sudo certbot --nginx -d example.com -d www.example.com。
  6. 确认 HTTPS 已生效,能拿到响应头、没有证书报错:curl -I https://example.com。
  7. 确认 HTTP 被跳到 HTTPS,返回 301 和 Location: https://example.com/:curl -I http://example.com。
  8. 演练续期,不会真的换证书:sudo certbot renew --dry-run。
  9. 确认自动续期的定时器在:systemctl list-timers | grep certbot。

Certbot 往 server 块里加了 listen 443 ssl 和指向 /etc/letsencrypt/live/example.com/ 的证书路径,并加了 80 端口跳转 HTTPS 的规则。证书有效期 90 天,定时器定期检查,剩不到 30 天时自动续期。本机的随书 compose 没有公网域名,申请不了证书。

配置文件的位置:

配置项 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 写。

  • 浏览器显示 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,后端框架打开信任代理头的设置。
  • 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 时照抄。