安装、升级与目录结构

0 阅读12分钟

这是《Nginx 从入门到精通》系列的第 2 篇(全系列 15 篇 + 4 个实战项目)。目标:在任意环境(Debian、RHEL、容器、自编译)装好、升级、回滚 Nginx;能独立读懂 nginx -V,并说清「什么时候必须源码编译」。

前置:第 1 篇(认识 Nginx)的 master/worker 模型与信号基础;实验复用 lab/00-env。 本文所有数字与输出均为 nginx 1.30.6(stable)实测,文末附自测题与延伸阅读。


1. 先跑起来(5 分钟实验)

两条命令看清"装的是什么",再起环境。先读编译参数——这是本章后面所有判断的依据:

docker run --rm --entrypoint nginx nginx:stable-alpine -V 2>&1 | head -5

预期输出(版本号随镜像更新,以 https://nginx.org/en/CHANGES 为准;本文实测时 nginx:stable-alpine = 1.30.6、nginx:alpine = 1.31.8):

nginx version: nginx/1.30.6
built by gcc 15.2.0 (Alpine 15.2.0)
built with OpenSSL 3.5.8 25 Aug 2026 (running with OpenSSL 3.5.9 29 Sep 2026)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf ... --with-compat --with-threads --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-http_stub_status_module --with-stream ...

注意 --entrypoint nginx:官方镜像的 entrypoint 会先跑 /docker-entrypoint.d/ 脚本,不绕过它就只能看到一堆 Launching ... 日志。然后起实验环境:

cd nginx/lab/00-env
docker compose up -d          # 5 个容器:gateway + app1/app2/app3
docker compose ps             # gateway 立即 running,app 约 10s 后 healthy
curl -s localhost:8080/healthz

预期输出:

gateway ok

宿主 8080 → 容器 80;网关主配置是挂载进来的 /etc/nginx/lab/nginx.conf,由 command 的 -c 指定。这条事实贯穿全章:在这个环境里,nginx -t 不带 -c 校验的是镜像自带的默认配置,等于没校验。


2. 核心概念

2.1 四条安装路径怎么选

路径典型命令 / 镜像版本升级方式什么时候选代价
发行版仓库apt install nginx、dnf install nginx滞后(常落后 1–2 个分支)包管理器换二进制 + reload只想跑静态站 / 反代,跟系统补丁走缺 HTTP/3、第三方模块难加;发行版还改了默认目录与 nginx.conf
nginx.org 官方源按 linux_packages.html 配 deb/rpm 源,可选 stable / mainline新(实测 stable 1.30.x、mainline 1.31.x)apt upgrade nginx / dnf update nginx + reload生产首选:新版本、官方目录约定、可预测需要自己固定版本、自己管 repo 优先级
官方 Docker 镜像nginx:stable-alpine、nginx:alpine同上,随 tag 滚动换镜像 + 重建容器本书实验、不可变基础设施、K8s Ingress 底座pid/日志/临时目录都在容器内;user 与权限坑(见坑 6)
源码自编译./configure && make && make install完全自选(含打补丁)USR2 热升级(见 2.4)要 HTTP/3、brotli、njs 等官方包未带的模块,或要自定义路径/编译选项自己管编译、自己管 CVE、自己管模块 ABI

两条附加路线:OpenResty(Nginx + LuaJIT + lua-resty-*,见 Ch11);rootless 镜像 nginxinc/nginx-unprivileged(以非 root uid 101 运行,默认监听 8080,pid 与临时文件挪到可写目录,适配 K8s runAsNonRoot 基线;具体路径以该镜像 README 为准)。

判断口径只有一句:能用包或镜像解决的,别自编译;自编译的每一份配置都要进 Git,并把 nginx -V 的输出一起存档——否则半年后没人知道这台机器上的 nginx 还能不能 load_module。

2.2 编译参数:nginx -V 是唯一可信源

nginx -V = 版本 + 全部 ./configure 参数(-v 只给版本)。它回答三个问题:哪些模块是静态编进去的(--with-*)、动态模块该放哪(--modules-path)、默认路径是什么(--prefix、--conf-path、--pid-path、--error-log-path)。

本章要认识的参数:

参数作用不写会怎样
--prefix安装根目录(决定默认 logs/、conf/ 位置)落到 /usr/local/nginx,与系统包打架(见坑 3)
--modules-path动态模块搜索目录,load_module modules/x.so; 的相对基准load_module 找不到文件(dlopen() ... failed)
--with-http_ssl_moduleHTTPS 基础,主流版本默认不带,必须显式开配置里出现 ssl_certificate 直接 unknown directive
--with-http_v2_moduleHTTP/2http2 on;(≥1.25.1)报 unknown directive
--with-http_v3_moduleHTTP/3(listen 443 quic,≥1.25.0 实验性)同上
--with-stream四层(TCP/UDP)代理stream { } 块报 unknown directive
--with-compat允许动态模块与主程序按不同选项编译仍可加载第三方 .so 加载时 ABI 不匹配(见坑 4)
--add-module第三方模块静态编入—
--add-dynamic-module第三方模块编成 .so,运行期 load_module—

实测:nginx:stable-alpine 1.30.6 的 -V 里已带 --with-http_v3_module。规划附录 A 写"官方镜像不含 HTTP/3"是旧信息;判断方法永远是现场跑 -V,不要背结论。

2.3 信号:Nginx 的运维接口

Nginx 没有配置 API,一切运行期操作都是给 master 发信号(nginx -s <sig> 只是读 pid 文件后 kill——所以 -s 不需要 -c,实测在 lab 网关里不带 -c 也能 reload 成功)。

信号-s 写法语义对存量连接典型用途
HUPreload重新读配置,起新 worker,老 worker 优雅退出不断(老连接由老 worker 服务完)改配置生效
QUITquit优雅停机服务完再退缩容、灰度下线
TERM / INTstop快速停机立即断卡死时的最后手段
USR1reopen重开日志文件不影响配合 logrotate(见坑 7)
USR2—启动新二进制的新 master(热升级)不影响换二进制
WINCH—让老 worker 优雅退出、master 保留逐步退热升级第 3 步

2.4 平滑升级与回滚(5 步)

顺序不能错,每一步都有可验证的产物:

#动作目的验证
0备份 conf + nginx -t -c <你的主配置>杜绝"带病升级"syntax is ok / test is successful
1换二进制,kill -USR2 $(cat /run/nginx.pid)老 master 把 pid 改名 nginx.pid.oldbin,fork 出新 master + 新 workerls /run/nginx.pid* 两个文件;ps 里两个 master process
2打流量验证(curl / 慢请求)确认新二进制在这套配置下真能干活状态码正常、日志字段正常
3kill -WINCH $(cat /run/nginx.pid.oldbin)老 worker 优雅退出,老 master 留着当退路ps 里老 worker 消失,服务不中断
4kill -QUIT $(cat /run/nginx.pid.oldbin)老 master 退出,nginx.pid.oldbin 被清理只剩一棵进程树

两个必须知道的前提(实测得来,见实验 5):① 老的 nginx 进程必须用绝对路径启动(/usr/sbin/nginx),否则 USR2 里 execve() 找不到 "nginx" 直接失败;② USR2 的目标是 master,容器里若 nginx 就是 PID 1,最后一步 QUIT 老 master 等于停容器——线上热升级要在"nginx 由 init/systemd 托管"的宿主机进程模型下做。

回滚:任何一步发现问题 → 给老 master 发 HUP(它用内存里的旧配置重启 worker,不读磁盘),再给新 master 发 QUIT。全程不重启机器、不断连接,这是 Nginx 敢在线上换二进制的底气。

2.5 目录约定:先认路再改配置

项目Debian/Ubuntu 包nginx.org / RHEL 系官方 Docker 镜像(本书)
主配置/etc/nginx/nginx.conf/etc/nginx/nginx.conf/etc/nginx/nginx.conf(本书被 -c 换成 /etc/nginx/lab/nginx.conf)
站点配置conf.d/、sites-available/ + sites-enabled/(软链)conf.d/*.confconf.d/*.conf
动态模块/usr/lib/nginx/modules + modules-enabled/*.conf/usr/lib64/nginx/modules/etc/nginx/modules → /usr/lib/nginx/modules(空目录)
站点根/var/www/html/usr/share/nginx/html/usr/share/nginx/html(本书挂载 lab/00-env/static)
日志/var/log/nginx/{access,error}.log同左/var/log/nginx/,本书写 lab_access.log / lab_error.log
pid/run/nginx.pid/run/nginx.pid/run/nginx.pid(本书配置写 /var/run/nginx.pid,Alpine 上二者是同一文件)
临时目录/var/lib/nginx/{client_body,proxy,...}/var/cache/nginx/*_temp/var/cache/nginx/*_temp

两个必须记住的差异:(1) 站点根不同——从 apt 装的环境把 root /usr/share/nginx/html; 抄过去会 403/404;(2) sites-enabled 是 Debian 的软链约定,include 的 glob 按字典序加载,多站点时顺序敏感(Ch3 用 -T 展开 include 顺序)。

2.6 静态模块 vs 动态模块

静态编入(--add-module、--with-*)动态模块(--add-dynamic-module → .so)
配置写法无load_module modules/ngx_xxx.so;(main 上下文,必须在 events/http 之前)
生效时机编译期每次 reload 都要能被加载
升级二进制必须整包重编换主程序后 .so 通常可复用(同大版本内)
忘写 load_module不存在此问题该模块的指令报 unknown directive,或 worker 启动即崩
适用核心功能、追求极简第三方/非核心:geoip、brotli、njs、vts

3. 指令详解

指令默认值上下文说明与坑
load_module无main只在 main;写进 http 里直接 directive is not allowed here。路径相对 --modules-path,也可写绝对路径。升级后漏这一行是最常见的"配置没变却起不来"
user编译参数 --user 决定(镜像 nginx,Debian 包 www-data)main只影响 worker。rootless 容器里写它反而 [emerg] getpwnam("nginx") failed
worker_processes1mainauto = CPU 核数。本书实验固定 1:轮询、缓存、日志完全可预测;多 worker 差异在 Ch12
worker_connections512events每 worker 连接上限,不是并发请求数:反代场景一个请求同时占"客户端 + 上游"两个连接
pid编译参数 --pid-path(/run/nginx.pid)main-s 与 USR2 都靠它找 master。热升级后同目录出现 nginx.pid.oldbin
daemononmain容器里必须 off(官方镜像启动时自动加 -g "daemon off;"),否则容器立刻退出
master_processonmain只在 gdb 单进程调试时设 off;off 后所有信号语义失效(没有 master)
worker_shutdown_timeout无(不限,≥1.11.11)mainreload/停机时老 worker 的兜底上限;长连接站不设,老 worker 可能挂几天不退,看起来像"内存泄漏"
worker_rlimit_nofile继承系统限制main抬 fd 上限(systemd 的 LimitNOFILE 要同步改),只改配置不改 limits 无效
include—任意include /etc/nginx/lab/conf.d/*.conf;;glob 无匹配不报错(静默失效的经典原因),按字典序加载
error_loglogs/error.log(镜像 /var/log/nginx/error.log)main,http,mail,stream,server,location级别 debug/info/notice/warn/error/crit;debug 需 --with-debug 编译,否则不生效也不报错
access_loglogs/access.log combinedhttp,server,location,if,limit_except,stream官方镜像的 access.log 是指向 /dev/stdout 的软链,tail -f 会卡住;本书写真实文件 lab_access.log

4. 完整配置示例

两段文件,与 lab/00-env 的两个文件一一对应。第一段 nginx/nginx.conf:

# ===== /etc/nginx/lab/nginx.conf(main + http 上下文)=====
# 相对 lab/00-env 只多了三处,均在下方标「新增」
user  nginx;
worker_processes  1;                 # 实验固定 1 worker,行为可预测

# 新增 1:fd 上限(配合容器/宿主 ulimit -n 使用)
worker_rlimit_nofile  65535;
# 新增 2:reload/停机时老 worker 的兜底退出上限(≥1.11.11)
worker_shutdown_timeout  30s;
# 新增 3:显式写 master,为热升级实验留出对照点(默认就是 on)
master_process  on;

error_log  /var/log/nginx/lab_error.log warn;
pid        /var/run/nginx.pid;        # 与镜像默认 /run/nginx.pid 是同一文件

# 第三方模块从这里加载(main 上下文,必须早于 events/http):
# load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
# 官方镜像的 modules 目录是空的:这一行开着却没有 .so,nginx -t 就报 dlopen() failed

events {
    worker_connections  1024;         # 单 worker,反代场景约等于 512 并发请求
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user "$request" '
                      '$status $body_bytes_sent rt=$request_time '
                      'urt=$upstream_response_time up=$upstream_addr '
                      'xff="$http_x_forwarded_for"';

    access_log  /var/log/nginx/lab_access.log  main;

    sendfile        on;
    keepalive_timeout  65;
    gzip on;
    gzip_types text/css application/javascript application/json text/plain;

    resolver 127.0.0.11 valid=5s ipv6=off;   # Docker 内置 DNS

    upstream app_pool {
        zone app_pool 64k;
        server app1:8080 resolve max_fails=3 fail_timeout=10s;
        server app2:8080 resolve max_fails=3 fail_timeout=10s;
        server app3:8080 resolve max_fails=3 fail_timeout=10s;
    }

    include /etc/nginx/lab/conf.d/*.conf;
}

第二段 nginx/conf.d/default.conf 与 lab 一致(宿主 8080 → 容器 80):

# ===== /etc/nginx/lab/conf.d/default.conf =====
server {
    listen 80;
    server_name _;

    root  /usr/share/nginx/html;
    index index.html;

    location = /healthz {
        access_log off;
        return 200 "gateway ok\n";
    }

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny  all;
    }

    location /static/ {
        alias /usr/share/nginx/html/;
        expires 7d;
        add_header Cache-Control "public";
    }

    location /slow/ {
        proxy_pass http://app_pool;
        proxy_set_header Host $host;
        proxy_read_timeout 5s;
    }

    location /api/ {
        proxy_pass http://app_pool/;
        proxy_connect_timeout 2s;
        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;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}

相对 lab/00-env 改了哪几行:新增 worker_rlimit_nofile 65535;、worker_shutdown_timeout 30s;、master_process on; 三行;load_module 一行是注释示例(装了第三方模块再放开)。其余(8080 映射、app_pool 三成员、lab_access.log、-c /etc/nginx/lab/nginx.conf)与 lab 一字不差;改完照 README 三步走:nginx -t -c … → nginx -s reload → curl /healthz。


5. 必做实验

实验 1:三条安装路径的 -V 对照

docker run --rm --entrypoint nginx nginx:stable-alpine -V 2>&1 | tr ' ' '\n' | grep -E '^--' | head -6
docker run --rm --entrypoint nginx nginx:alpine        -v 2>&1

预期:

--prefix=/etc/nginx
--sbin-path=/usr/sbin/nginx
--modules-path=/usr/lib/nginx/modules
--conf-path=/etc/nginx/nginx.conf
--error-log-path=/var/log/nginx/error.log
--http-log-path=/var/log/nginx/access.log
nginx version: nginx/1.31.8

为什么:stable 用偶数码、mainline 用奇数码(实测同一时刻 1.30.6 与 1.31.8),只有 mainline 第一时间拿到新指令;同一台机器上两份二进制谁在跑,也只能靠 -V 区分。

实验 2:读懂 -t 的两种报错

mkdir -p /tmp/ngxcheck && cat > /tmp/ngxcheck/bad.conf <<'EOF'
events {}
http {
    worker_connections 1024;
    server {
        listen 8080;
        brotli on;
        location / { return 200 "ok\n"; }
    }
}
EOF
docker run --rm -v /tmp/ngxcheck:/c:ro nginx:stable-alpine nginx -t -c /c/bad.conf

预期(报第一个错就停,所以先看到上下文错):

nginx: [emerg] "worker_connections" directive is not allowed here in /c/bad.conf:3
nginx: configuration file /c/bad.conf test failed

把 worker_connections 1024; 挪回 events 再跑,第二条错误才出现:

nginx: [emerg] unknown directive "brotli" in /c/bad.conf:6
nginx: configuration file /c/bad.conf test failed

为什么:directive is not allowed here 是放错上下文(worker_connections 只属于 events),unknown directive 是没编/没加载这个模块(brotli 不在官方镜像里,正是 2.6 与坑 4 的场景)。两种错指向完全不同的修法,Ch3 会把上下文规则讲透。

实验 3:目录勘察(不进容器也能看清布局)

docker run --rm --entrypoint sh nginx:stable-alpine -c 'ls /etc/nginx; ls -ld /etc/nginx/modules; ls /usr/share/nginx/html'

预期:

conf.d  fastcgi.conf  fastcgi_params  mime.types          # ls /etc/nginx
modules nginx.conf    scgi_params     uwsgi_params
lrwxrwxrwx 1 root root 22 Oct 21 23:13 /etc/nginx/modules -> /usr/lib/nginx/modules
50x.html  index.html                                      # ls /usr/share/nginx/html

为什么:modules 是指向 /usr/lib/nginx/modules 的软链、且目录是空的——这解释了为什么动态模块必须自己编译再放进去;/usr/share/nginx/html 里有现成的 index.html/50x.html,所以镜像里 root /usr/share/nginx/html; 能直接出页,而 Debian 包上要改成 /var/www/html。

实验 4:reload 与 reopen 的真实效果

cd nginx/lab/00-env
docker compose exec -T gateway nginx -t -c /etc/nginx/lab/nginx.conf
docker compose exec -T gateway ps -o pid,ppid,args        # busybox ps 不支持 -C / cmd
docker compose exec -T gateway nginx -s reload
sleep 1
docker compose exec -T gateway ps -o pid,ppid,args
docker exec lab-gateway sh -c 'mv /var/log/nginx/lab_access.log /var/log/nginx/lab_access.log.bak'
docker compose exec -T gateway nginx -s reopen            # 等价 kill -USR1
docker exec lab-gateway ls -l /var/log/nginx/ | grep lab_access

实测输出(reload 前后):

nginx: the configuration file /etc/nginx/lab/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/lab/nginx.conf test is successful
    1     0 nginx: master process nginx -c /etc/nginx/lab/nginx.conf -g daemon off;
   66     1 nginx: worker process
    1     0 nginx: master process nginx -c /etc/nginx/lab/nginx.conf -g daemon off;
  156     1 nginx: worker process
-rw-r--r--    1 nginx    root             0 Nov  3 04:20 lab_access.log
-rw-r--r--    1 root     root         36258 Nov  3 04:19 lab_access.log.bak

为什么:master 的 pid(1)不变、worker 的 pid(66 → 156)换代——所以内存里的共享内存 zone(app_pool 那 64k)跨 reload 保留,这正是缓存与限流计数能在 Ch8 里活下去的前提。reopen 让 nginx 重新 open() 日志路径、造出 0 字节新文件(属主 nginx),旧文件里 36KB 内容留在 .bak 中——这就是 logrotate 的标准动作。

实验 5:完整热升级 + 回滚(一次性容器,不污染 lab)

docker run -d --name ngxup nginx:stable-alpine sh -c '/usr/sbin/nginx; sleep 3600'
sleep 2
docker exec ngxup ps -o pid,ppid,args | head -4          # master=8,worker=9
# 1) 起新 master
docker exec ngxup sh -c 'kill -USR2 $(cat /run/nginx.pid); sleep 2; ls /run/nginx.pid*; ps -o pid,ppid,args | head -8'
# 2) 老 worker 优雅退出
docker exec ngxup sh -c 'kill -WINCH $(cat /run/nginx.pid.oldbin); sleep 1; ps -o pid,ppid,args | head -6'
# 3) 老 master 退出
docker exec ngxup sh -c 'kill -QUIT $(cat /run/nginx.pid.oldbin); sleep 1; ls /run/nginx.pid*; ps -o pid,ppid,args | head -4'
docker rm -f ngxup

实测关键片段:

# USR2 之后:两个 pid 文件、两棵进程树
/run/nginx.pid  /run/nginx.pid.oldbin
    8     1 nginx: master process /usr/sbin/nginx
   34     8 nginx: master process /usr/sbin/nginx   # 新 master 是老 master 的子进程
   35    34 nginx: worker process
# WINCH 之后:老 master(8) 仍在,老 worker 9..13 全退,只剩新树
    8     1 nginx: master process /usr/sbin/nginx
   34     8 nginx: master process /usr/sbin/nginx
# QUIT 之后:老 master 变僵尸 [nginx],pid 文件只剩一个
/run/nginx.pid                       # 8 -> [nginx],34 被 init 接管(PPID 变 1)

为什么:USR2 只 fork 新 master、不碰老 worker;WINCH 才让老 worker 停接新连接、服务完存量再退——所以升级窗口内请求不会断。回滚练法:在第 1 步之后给老 master 发 kill -HUP 8(用内存旧配置重启 worker),再 kill -QUIT 34,ps 就回到单棵树。注意实测踩到的坑:把 nginx 写成相对路径启动时,USR2 会失败——[alert] execve() failed while executing new binary process "nginx" (2: No such file or directory),因为 re-exec 用的是 argv[0],不做 PATH 查找。


6. 验收自测(5 题)

Q1. 写出平滑升级的 5 个步骤(含信号),并说明某一步出问题怎么回滚。

答案要点:备份 conf → nginx -t 通过 → 换二进制 → USR2(生成 nginx.pid.oldbin、新 master 起来)→ 打流量验证 → WINCH 老 worker → QUIT 老 master(清理 oldbin)。回滚只要两条:给老 master HUP(用内存里的旧配置重启 worker,不读磁盘),再给新 master QUIT;不需要重装、不需要断连接。

Q2. 举出 3 个"必须源码编译"的场景,并说明判断依据。

答案要点:①要官方包未带的第三方模块(brotli 压缩、njs、vts 指标)——只能 --add-dynamic-module 编出 .so 再 load_module;②要非默认编译选项(某些发行版包没有 --with-http_v3_module,没开 --with-compat 时第三方 .so 加载失败);③要打补丁或自定义路径(--prefix=/opt/nginx,日志/临时目录不在 /var)。判断依据永远是 nginx -V:参数里没有的,配置里一定用不了。

Q3. 本实验环境里为什么 nginx -t 必须带 -c /etc/nginx/lab/nginx.conf?不带会发生什么?

答案要点:主配置被 command 的 -c 改到了挂载目录,默认的 /etc/nginx/nginx.conf 仍然是镜像自带那份。不带 -c 时校验的是默认配置——同样会打印 test is successful,于是你以为校验过了,实际你改的 conf.d/default.conf 根本没参与校验;等 -s reload 才在 error.log 里看到 [emerg]。这就是 lab README 把它列为头号排障项的原因。

Q4. 升级后配置里出现 unknown directive "brotli",可能原因有哪些?

答案要点:①换二进制后漏了 load_module(模块没加载,指令就成了"未知");②load_module 写了但 .so 不在 --modules-path 下,此时报 dlopen() ... failed;③新二进制没带 --with-compat 或跨了大版本,.so ABI 不兼容(worker 起不来);④真的没编这个模块。排查顺序:nginx -V 比参数 → ls 模块目录 → -t 看报错位置。

Q5. HUP、USR1、TERM 分别用在什么场景?为什么 reload 不掉连接而 stop 会?

答案要点:HUP = 改配置生效(master 不变、worker 换代,存量连接由老 worker 服务完);USR1 = 重开日志(logrotate 每天一次的标准动作,不做就会把磁盘写满);TERM = 快速停机,直接断连接。reload 不掉连接,是因为老 worker 进入"停接新连接、服务完存量再退"的优雅退出状态,stop 没有这一步。


7. 常见坑

  1. nginx -t 通过、reload 后行为没变 → 根因:校验的不是正在跑的那份配置(漏 -c,或改的是另一个目录里的同名文件)。正解:ps -o pid,ppid,args 看进程实际用的 -c,再用 nginx -T 确认最终生效内容。
  2. 从 apt 环境抄了 root /usr/share/nginx/html;,访问 403/404 → 根因:Debian 包站点根是 /var/www/html(见 2.5)。正解:grep -r "root " /etc/nginx/sites-enabled/ 对一遍,或统一用 nginx -T 看生效值。
  3. 机器上装了两份 nginx(apt 一份 + 源码 --prefix=/usr/local 一份),-t 报错在 A 文件、服务表现却是 B 配置 → 根因:nginx 命令命中的是另一个二进制、另一个 --conf-path。正解:which -a nginx + nginx -V 对 --conf-path,只留一个;容器化后这类问题消失。
  4. 热升级后 worker 起来就崩,日志里是 [emerg] 或段错误 → 根因:动态模块与新二进制不是同一版本源码,或主程序未带 --with-compat,ABI 不匹配。正解:同大版本内升级,.so 与主程序一起重编;升级前先起临时实例打流量验证(实验 5 的临时容器就是模板)。
  5. kill -USR2 没反应,或出现两个 master 后忘了收尾 → 根因有三个,实测都能复现:①老进程用相对路径 nginx 启动,re-exec 报 execve() failed;②容器里 nginx 就是 PID 1,QUIT 老 master = 停容器;③只做了 USR2 没做 WINCH/QUIT,内存与 fd 双倍占用。正解:用绝对路径启动、在 systemd/init 托管的宿主机进程模型下演练,并把 5 步写成清单,收尾必做 ls /run/nginx.pid*。
  6. 容器里配了 user nginx; 启动失败 [emerg] getpwnam("nginx") failed → 根因:rootless 镜像里没有该用户(uid 101),非 root 也无权 setuid。正解:rootless 场景删掉 user,把 pid、*_temp、日志指到可写目录(如 /tmp)。
  7. tail -f /var/log/nginx/access.log 卡住、日志"没了" → 根因:官方镜像该文件是指向 /dev/stdout 的软链,被 mv/logrotate 之后 tail 跟丢了 inode。正解:本书写真实文件 lab_access.log;需要切割时 mv 之后必须 nginx -s reopen,否则 fd 还指着被移走的 inode,磁盘只涨不落(实验 4 已实测)。
  8. 把 mainline 当 stable 装进生产 → 根因:只记得"官方源默认给最新"。正解:apt-cache policy nginx / dnf repoquery --show-duplicates nginx 看候选版本,稳定站锁 stable 源;升级窗口与 Ch14 的 -T 配置快照 diff 一起做。

8. 延伸阅读

  1. 各平台官方安装包(deb/rpm 源,stable 与 mainline 的取舍):nginx.org/en/linux_pa…
  2. 源码编译与依赖说明:nginx.org/en/docs/ins…
  3. 命令行参数(-t / -T / -V / -s / -g):nginx.org/en/docs/swi…
  4. 运行期控制:全部信号与热升级完整步骤:nginx.org/en/docs/con…
  5. ./configure 全部参数(--with-compat、--add-dynamic-module 等):nginx.org/en/docs/con…
  6. core 模块参考(load_module、user、pid、daemon、worker_*):nginx.org/en/docs/ngx…
  7. 官方 Docker 镜像与 rootless 变体:github.com/nginx/docke…、github.com/nginxinc/do…

相关章节:Ch1 · Ch3 · Ch5 · Ch11 · Ch12 · Ch14


系列导航:第 1 篇进程模型与能力边界 → 第 2 篇(本篇)安装、升级与目录结构 → 第 3 篇配置语法 → 第 4 篇静态站与 location → 第 5 篇反向代理 … 第 15 篇故障案例库。