Docker 安装 Nginx:LNMP 容器网络与 FastCGI 排错

发布时间:2026/10/26 12:12:09
Docker 安装 Nginx:LNMP 容器网络与 FastCGI 排错 LNMP 这套组合里Nginx 通常是第一个被装上去、也是最后一个被真正调通的那个。MySQL 只要端口能连上、账号能登进去基本就完事了PHP 容器跑起来能看到 phpinfo也算阶段性胜利唯独 Nginx配置文件看着都对容器也在跑浏览器一打开就是 403、404 或者 502日志里翻半天也看不出个所以然。这一篇就专门讲 Docker 环境下安装 nginx 这件事从镜像怎么选、目录怎么挂、配置怎么写到它和 PHP 容器之间那条 FastCGI 通道怎么打通以及我在这台机器上真实踩过的几个坑全部摊开说。适合已经跑起了 MySQL 和 PHP 容器、正准备把 Web 入口接上的朋友也适合刚开始用容器化方式搭环境、想搞清楚每一步在干什么的新手——下面这些命令和配置都可以直接抄但每一段为什么这么写我会一并交代清楚。1. Nginx 在这次容器组合里到底站在哪个位置1.1 三个容器之间的职责边界先划清楚用 Docker 搭 LNMP最忌讳的一件事就是脑子里还是一台机器装三个软件的老模型。容器化之后这三个东西是三个独立的运行单元各自有独立的文件系统、独立的进程空间它们之间唯一的联系是 Docker 网络和共享挂载的目录。所以动手装 Nginx 之前我习惯先在纸上把边界画清楚Nginx 容器负责监听 80 端口、接收 HTTP 请求、处理静态文件、把 .php 结尾的请求转发给 PHP 容器PHP 容器只负责跑 php-fpm监听 9000 端口等 Nginx 把请求递过来它自己不需要对外暴露任何端口MySQL 容器只跟 PHP 容器打交道Nginx 从头到尾都不会跟数据库说一句话。这个边界划清楚之后很多看起来莫名其妙的问题就有了解释。比如你在浏览器里看到一个 PHP 报错说连不上数据库那这个请求已经顺利穿过了 Nginx 和 PHP 两层问题只可能出在 PHP 容器到 MySQL 容器那一段跟 Nginx 配置没有半点关系。反过来如果浏览器返回的是 502那说明请求到了 Nginx但 Nginx 没能从 PHP 那边拿到有效响应问题在两者之间的通道上跟数据库更没关系。这种按层定位的思路比对着日志一行行猜要高效得多也是后面所有排错工作的基础。我见过不少人把 Nginx 和 PHP 装在同一个容器里图省事镜像里又是 nginx 又是 php-fpm 还要装 supervisord 管进程。这种做法在小 Demo 阶段确实跑得起来但它把两套完全不同的生命周期绑在了一块改一行 Nginx 配置要重启整个容器PHP 那边 OOM 了 Nginx 也跟着挂。分开之后改配置就是改挂载文件加一次 reload两边互不影响升级也是各自升级各自的。1.2 为什么不直接复用宿主机上的 Nginx一个很自然的想法是宿主机上本来就装着 Nginx让它监听 80把 .php 请求转发到 PHP 容器的 9000 端口不就完了何必再起一个容器。技术上完全可行但我实际用下来更倾向于把 Nginx 也放进容器原因有三个。第一是路径一致性。Nginx 转发 PHP 请求时传给 php-fpm 的参数里有一个SCRIPT_FILENAME这是 PHP 容器内部要去读的文件路径。如果 Nginx 跑在宿主机上它看到的项目路径可能是/home/xxx/www/project而 PHP 容器里这个目录挂载在/var/www/html两边路径对不上php-fpm 就会告诉你Primary script unknown然后返回 404。要么你在宿主机上把项目目录软链到跟容器内一模一样的路径要么你在 Nginx 配置里做路径映射怎么看都比直接把 Nginx 也容器化要别扭。第二是环境一致性。宿主机上的 Nginx 版本、编译参数、模块列表是固定的换一台机器部署可能就不一样了。容器化之后镜像标签写死版本谁跑都是同一份二进制、同一份默认配置出问题好复现。第三是清理成本。实验性质的搭建玩完了docker compose down一条命令全清掉宿主机上不留任何痕迹。用宿主机的 Nginx配置散落在/etc/nginx/conf.d日志散落在/var/log/nginx时间一长自己都记不清哪个文件是干什么的。1.3 端口和网络拓扑要在动手前定死装任何容器服务端口规划这件事我建议写在第一次动手之前写在备忘录里都行。因为一旦跑起来再去改涉及的挂载、配置、防火墙规则全要跟着改很烦。我这次用的规划是这样的服务容器内端口宿主机映射是否对外开放nginx808080是浏览器访问入口nginx4438443先留着证书配好再启用php-fpm9000不映射否只在容器网络内可达mysql330633060仅本机 127.0.0.1调试用宿主机 80 端口之所以不用是因为我这台机器上还跑着别的东西80 已经被占了用 8080 做映射浏览器访问http://127.0.0.1:8080一样的效果。这里有个细节值得说一句映射端口写成-p 8080:80的时候前面的 8080 是宿主机端口后面的 80 是容器内端口别写反了。写成-p 80:8080的话容器里 Nginx 监听的是 80你在宿主机上访问 8080 是永远打不开的这个方向搞反是新手最常见的错误之一。网络方面我不用默认的 bridge而是自己建一个自定义网络。原因很简单Docker 默认 bridge 网络里的容器之间只能用 IP 互相访问容器重启后 IP 会变Nginx 配置里写死的fastcgi_pass 172.17.0.5:9000下次就失效了。自定义网络支持用容器名做 DNS 解析写fastcgi_pass php:9000就行IP 怎么变都不影响。docker network create --driver bridge lnmp-net docker network ls | grep lnmp这条命令执行完之后网络就建好了。后面启动 nginx、php、mysql 三个容器的时候都加上--network lnmp-net它们就自动在同一个网络里能用名字互相找到。2. 镜像标签怎么选以及拉取不顺时的处理顺序2.1 alpine 版和完整版的取舍官方 nginx 镜像的标签挺多的latest、stable、mainline、alpine以及各种版本号组合第一次看确实容易懵。我自己的选择规则很朴素生产或者类生产环境用带具体版本号的 alpine 版本本地调试图省事可以用完整版。alpine 版本体积小nginx:1.25-alpine拉下来压缩后大概十几兆解压后四十兆左右启动快攻击面也小。代价是它基于 musl libc如果你后面想加一些需要 glibc 的第三方模块或者想在里面跑一些依赖系统库的调试工具可能会遇到麻烦。完整版基于 Debian体积大一些但工具链齐全apt、bash、curl都有排查问题的时候舒服。这两个版本的对比我列了一下镜像标签解压后体积适合的场景需要注意的地方nginx:1.25-alpine约 40MB长期运行、资源紧张musl libc编译第三方模块不便nginx:1.25约 190MB调试期、需要临时装工具体积大启动稍慢nginx:stable跟随稳定分支想要稳定但懒得记版本号标签会漂移不推荐用于生产nginx:latest不确定不建议某天拉下来可能就变了个大版本latest这个标签我是真的不建议用。它今天指向 1.25半年后可能指向 1.27某次重新拉镜像或者换机器部署配置文件里某个指令被废弃了容器直接起不来而你根本不知道是哪里变了。版本号写死升级变成一个有意识的操作而不是被动接受。2.2 拉取慢、拉取失败时的排查顺序镜像拉取这一步不同网络环境下体验差异很大。我遇到的失败情况大概分三类处理顺序是从外到内一层层看。第一类是超时卡在Pulling from library/nginx半天不动。这种情况先确认基础连通性curl -I https://registry-1.docker.io/v2/看能不能通。如果连不上多半是本机的网络出口有问题跟 Docker 配置没关系。如果能通但依然很慢那就是大文件传输的问题考虑配置镜像加速地址这个在系列的前几篇里应该已经配过了/etc/docker/daemon.json里加上 registry-mirrors 然后systemctl reload docker。第二类是拉下来了但解压报错比如failed to register layer: no space left on device。这是磁盘满了df -h一看便知。Docker 的默认存储目录在/var/lib/docker如果根分区给得小装几个镜像就撑满了。处理办法要么清掉不用的镜像docker image prune -a要么把>docker pull nginx:1.25-alpine docker image inspect nginx:1.25-alpine --format {{.Architecture}} {{.Os}}拉取成功之后别急着run先看一眼镜像的默认配置。docker image inspect nginx:1.25-alpine --format {{json .Config}}能看到默认的入口命令、暴露端口、工作目录。这些信息决定了你后面怎么覆盖启动参数。2.3 镜像里的默认配置文件长什么样在挂载自己的配置之前我强烈建议先进去看一眼官方镜像自带的配置不然你根本不知道自己覆盖掉的是什么。docker run --rm nginx:1.25-alpine cat /etc/nginx/nginx.conf输出里能看到几个关键点user nginx;表示 worker 进程以 nginx 用户运行include /etc/nginx/conf.d/*.conf;在 http 块里意思是在 conf.d 目录下所有 .conf 结尾的文件都会被当作 http 级别的配置包含进来还有一个include /etc/nginx/mime.types;管的是各种文件扩展名对应的 Content-Type。另外/etc/nginx/conf.d/default.conf是镜像自带的默认站点配置监听 80root 指向/usr/share/nginx/html。知道这些之后再挂载心里就有数了如果我把自己写的default.conf挂到/etc/nginx/conf.d/default.conf镜像自带的那个就被覆盖了如果我挂载整个conf.d目录那目录里的所有文件都以我的为准。这两者的区别在后面的坑里会详细讲先记住结论就行。3. 启动命令里每一个参数的作用3.1 目录结构先规划好再写挂载写docker run之前我习惯先把项目目录在宿主机上建好。目录结构清晰后面写挂载参数就是一比一映射不容易搞混。mkdir -p ~/lnmp/nginx/conf.d mkdir -p ~/lnmp/nginx/logs mkdir -p ~/lnmp/www mkdir -p ~/lnmp/php建出来长这样~/lnmp/ ├── nginx/ │ ├── nginx.conf # 主配置覆盖镜像默认的 │ ├── conf.d/ │ │ └── default.conf # 站点配置 │ └── logs/ # 访问日志和错误日志 ├── php/ │ └── Dockerfile # 如果前面几篇是自己构建的 PHP 镜像 └── www/ # 项目代码nginx 和 php 都要挂载这里最关键的一条~/lnmp/www这个目录Nginx 容器和 PHP 容器必须挂载到容器内完全相同的路径。我一般统一用/var/www/html。这是前面提到的SCRIPT_FILENAME问题的根源所在——Nginx 计算出来的脚本路径会原样传给 php-fpm如果 PHP 容器里那个路径不存在就是 404。3.2 一条完整的启动命令逐段拆解docker run -d \ --name nginx \ --network lnmp-net \ --restart unless-stopped \ -p 8080:80 \ -p 8443:443 \ -v ~/lnmp/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ -v ~/lnmp/nginx/conf.d:/etc/nginx/conf.d:ro \ -v ~/lnmp/nginx/logs:/var/log/nginx \ -v ~/lnmp/www:/var/www/html:ro \ -v /etc/localtime:/etc/localtime:ro \ nginx:1.25-alpine-d是后台运行这个不解释。--name nginx这个容器名很重要因为同一网络里的其他容器要拿它做 DNS 解析写成fastcgi_pass nginx:9000这种就说明名字用错了应该是 php。名字一旦定了后面所有配置里的上游地址都得跟它对上。--network lnmp-net接上刚才建的网桥。--restart unless-stopped表示除非手动停否则容器挂了就自动拉起来——服务器断电重启之后这三个容器会自己回来不用你手动一个个start。我吃过没写这个的亏某次机器意外重启以为是代码问题查了半小时最后发现容器压根没起来。-p 8080:80和-p 8443:443是端口映射前面已经说过方向。-v那几行是挂载要注意:ro这个后缀它表示容器内对该目录只有读权限。配置文件、代码目录加上:ro是个好习惯万一容器里跑了什么奇怪的进程也改不了宿主机上的文件。日志目录不能加:ro因为 Nginx 要往里写。-v /etc/localtime:/etc/localtime:ro这一行是为了让容器内的时区跟宿主机一致。不加的话日志里的时间戳是 UTC跟你的本地时间差八小时排查问题时对不上时间线非常难受。最后是镜像名。整条命令执行完docker ps能看到 nginx 容器状态是 Up就说明启动成功了。如果状态是Restarting或者Exited立刻docker logs nginx看原因八成是配置文件写错了。3.3 用 compose 管理会更省心同样的东西用 docker compose 描述可读性和可维护性都会好很多。前面几篇如果是用 compose 起的 MySQL 和 PHP这里直接加一个 nginx 服务就行。services: nginx: image: nginx:1.25-alpine container_name: nginx restart: unless-stopped ports: - 8080:80 - 8443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/logs:/var/log/nginx - ./www:/var/www/html:ro - /etc/localtime:/etc/localtime:ro networks: - lnmp-net depends_on: - php networks: lnmp-net: external: truedepends_on这一行值得单独说一下。它保证 compose 启动时先起 php 再起 nginx顺序上是对的但它不保证 php 已经准备好接受连接只是保证容器进程起来了。这一点在后面的 502 章节会展开。另外external: true表示这个网络是外部已经存在的compose 不会去创建它也不会在down的时候删掉它配合前面手动docker network create的用法刚好。4. 配置文件的拆分逻辑主配置管骨架站点配置管业务4.1 nginx.conf 里只留全局性的东西很多人一上来就把整个站点配置塞进nginx.conf几百行堆在一起改一次要往下翻半天。我的做法是把nginx.conf当成骨架只放跟具体站点无关的全局设置站点相关的全部拆到conf.d下面。user nginx; 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; log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; server_tokens off; client_max_body_size 20m; include /etc/nginx/conf.d/*.conf; }逐项说一下为什么这么写。worker_processes auto让 Nginx 按 CPU 核心数自动决定 worker 数量比手写死一个数字更适应不同机器。sendfile on开启零拷贝静态文件传输效率会明显好一些尤其在容器里挂载了宿主机目录的场景这个选项的影响挺直观。server_tokens off是把响应头里的版本号藏掉Server: nginx/1.25.3变成Server: nginx。这不是什么高深的安全措施但能减少一点信息暴露加上也没成本。client_max_body_size 20m这一行是必须提前想好的。Nginx 默认只允许 1MB 的请求体超过就返回 413。你上传一个稍大的图片、传个 Excel全都卡在这里。很多人排查上传失败查了半天 PHP 的upload_max_filesize其实第一道关卡在 Nginx 这里就拦下了。这个值设多少取决于你的业务20m 对我这个项目够用了。注意 PHP 那边也有对应的upload_max_filesize和post_max_size两边要匹配Nginx 这边小的话PHP 配多大都没用。最后那个include /etc/nginx/conf.d/*.conf;放在 http 块末尾把站点配置引进来。顺序上它必须在 http 块内部如果写到外面去Nginx 启动直接报错server directive is not allowed here。4.2 default.conf 的 server 块逐行解释站点配置我一般写在这个文件里server { listen 80; server_name localhost; root /var/www/html; index index.php index.html index.htm; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.ht { deny all; } access_log /var/log/nginx/default.access.log main; error_log /var/log/nginx/default.error.log warn; }listen 80是容器内监听跟宿主机的 8080 没关系别写成 8080。root /var/www/html必须与挂载点、PHP 容器的挂载点三者完全一致。这三个地方只要有一个不一样就是 404 或者Primary script unknown。index index.php index.html的顺序有讲究。把 index.php 放前面是因为这种项目通常以 PHP 为入口如果放后面访问目录时 Nginx 会先找 index.html找到了就不走 PHP 了你改的 PHP 代码怎么都不生效这种问题查起来很折磨人。try_files $uri $uri/ /index.php?$query_string;这一行是伪静态的核心。它的逻辑是先看请求的 URI 对应文件存不存在存在就直接返回不存在就看是不是个目录是就找目录下的索引文件都不行就把请求交给/index.php处理并把原始查询串带上。很多框架的路由都靠这一行工作缺了它除了首页以外全是 404。location ~ \.php$这个正则块匹配所有以 .php 结尾的请求把它们转给 FastCGI 处理。这里的~表示区分大小写的正则匹配如果你担心有人用.PHP大小写绕过可以写成~*。fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;这一行是最容易漏的。官方镜像里的fastcgi_params文件内容并不包含SCRIPT_FILENAME它只有QUERY_STRING、REQUEST_METHOD这些。所以你必须自己手动加一行否则 php-fpm 收到的SCRIPT_FILENAME是空的它会去找一个不存在的路径然后返回Primary script unknown浏览器看到的就是 404。这个问题我见过太多次了网上一搜一大片根源就是漏了这一行。注意这个fastcgi_param要写在include fastcgi_params;前面。因为 include 会把一堆参数塞进来如果后面又有同名参数后写的会覆盖前面的。虽然fastcgi_params里没有SCRIPT_FILENAME顺序上其实无所谓但养成自定义参数在前、include 在后的习惯能避免很多隐性覆盖问题。4.3 fastcgi_pass 里的主机名是在什么时候被解析的fastcgi_pass php:9000;里的php是容器名靠 Docker 内置 DNS 解析成 IP。这里有个非常关键、也非常容易踩的细节Nginx 是在启动时解析 upstream 主机名的不是在请求时。也就是说如果 nginx 容器启动的时候php 容器还不存在或者不在同一个网络里Nginx 会直接启动失败日志里是host not found in upstream php然后容器进入重启循环。这个行为和很多人想象的运行时才连不一样。这就带来一个实操上的顺序要求必须先启动 php 容器再启动 nginx 容器。用 compose 的depends_on可以保证顺序但如果你是手动docker run一定要留意。更麻烦的情况是 php 容器后来被删掉重建了比如改了镜像而 nginx 容器一直在跑那 nginx 里的 DNS 缓存可能还指向旧 IP请求就全 502 了。解决办法是重建 php 之后顺手docker restart nginx让它重新解析一次。如果实在不想受这个限制有两种做法。一种是引入变量加 resolverresolver 127.0.0.11 valid10s; location ~ \.php$ { set $php_upstream php:9000; fastcgi_pass $php_upstream; ... }用了变量之后Nginx 就变成运行时解析了配合resolver 127.0.0.11这是 Docker 内置 DNS 的固定地址和较短的valid时间能适应 php 容器 IP 变化。代价是每次请求都多一次 DNS 查询虽然本地开销很小但配置复杂度上去了。我个人的选择是老老实实保证启动顺序简单直接出问题的概率反而更低。5. 静态资源打不开时的分叉排查5.1 403 和 404 各自指向什么问题配置写完docker restart nginx浏览器访问结果不是想要的页面。这时候别急着乱改先看返回的状态码它已经把问题范围缩得很小了。状态码最可能的原因快速验证方式403 Forbidden目录下没有 index 文件且目录浏览被禁止或文件权限不足在 root 目录下放一个 index.html 试试404 Not Foundroot 路径不对文件确实不存在伪静态规则缺失docker exec nginx ls /var/www/html看文件在不在502 Bad Gatewayphp 容器没起来、端口不对、SCRIPT_FILENAME 错误看 error.log 里的具体报错504 Gateway Timeoutphp 执行超时或 fastcgi 读超时设置太短看 PHP 那边是不是在跑耗时任务403 最常见的原因是 root 目录下确实没有 index 文件或者 index 里配置的文件名跟实际文件对不上。比如你配了index index.php但目录里只有index.htmlNginx 找不到索引文件又没开autoindex on就只能返回 403。另一种 403 是文件权限问题Nginx 的 worker 进程以 nginx 用户运行如果挂载进来的文件属主是 root 且权限是 600nginx 用户读不了也是 403。容器里跑docker exec nginx ls -l /var/www/html就能看到属主和权限。404 则要分两种情况。一种是文件真的不在docker exec进去ls一下就知道了。另一种是文件在但 Nginx 找的路径不对——这就回到那个老问题root配置的路径和实际挂载路径是否一致。我遇到过最隐蔽的一次是末尾多了个斜杠root /var/www/html/;和root /var/www/html;在配合location前缀匹配时行为不一样会导致 URI 拼接出现双斜杠或者路径错位。虽然多数情况下 Nginx 会容错但在alias场景下这个差异是致命的。用root的时候老老实实不加末尾斜杠。5.2 代码目录的挂载一致性检查清单前面反复强调三个路径要一致这里给一个具体的自查流程出问题时按顺序走一遍基本能定位。第一步看宿主机上的文件在不在ls -l ~/lnmp/www/确认index.php或index.html确实存在。第二步看 nginx 容器里能不能看到docker exec nginx ls -l /var/www/html/。看不到就是挂载参数写错了docker inspect nginx --format {{json .Mounts}}能看到实际的挂载映射对着检查。第三步看 php 容器里能不能看到docker exec php ls -l /var/www/html/。这里同样要能看到文件而且路径字符串必须跟 nginx 配置里的root完全一样。nginx 那边是/var/www/htmlphp 这边挂到了/var/www/html两边对上SCRIPT_FILENAME才能被 php-fpm 正确打开。第四步看 php-fpm 进程能不能读到。docker exec php ps aux | grep php-fpm看 worker 进程跑在哪个用户下再对比文件的权限位。如果 php-fpm 是以 www-data 用户跑的而文件是 600 且属主是 root那一样读不了。这四步走完静态文件和 PHP 文件的路径问题基本都能揪出来。我习惯把这几条命令存成一个脚本出问题直接跑一遍比在浏览器和终端之间来回切要快。5.3 autoindex 要不要开调试阶段把autoindex on;打开能救命。目录里到底有什么文件浏览器里一眼看到不用反复docker exec。但这个东西上线前必须关掉它会把目录结构暴露给任何人属于典型的信息泄露。我自己的做法是在conf.d下单独放一个debug.conf里面就一个autoindex on;调试的时候 include 进来调完把文件删掉或者注释掉。这样不会污染主配置也不容易忘。6. PHP 通道打不通502 与超时的几种来源6.1 502 的四个常见来源502 是 Nginx 明确告诉你我连不上上游。按可能性从高到低排大概这么几种。第一种php 容器根本没在跑或者不在同一个网络。docker ps看 php 容器的状态docker inspect php --format {{json .NetworkSettings.Networks}}看它接的是不是lnmp-net。两个容器不在同一个网络里名字解析不了自然连不上。这个错误在 error.log 里的表现是connect() failed (111: Connection refused) while connecting to upstream。第二种端口写错了。php-fpm 默认监听 9000但如果你前面几篇里改过www.conf里的listen比如改成listen 127.0.0.1:9000那它就只监听容器内的回环地址nginx 从外面连不进来。这种情况 error.log 里同样是 connection refused但docker exec php netstat -tlnp一看就明白监听地址是127.0.0.1:9000还是0.0.0.0:9000。必须是后者。第三种SCRIPT_FILENAME 路径不对。这个在 Nginx 看来连接是成功的但 php-fpm 返回 Primary script unknown 或者 Unable to open primary scriptNginx 把它转成 502 或 404 给浏览器。error.log 里能看到 File not found 的具体路径一眼就能判断是不是路径没对上。第四种SELinux 或者 AppArmor 拦了。在一些安全策略比较严的发行版上容器挂载宿主机目录之后php-fpm 去读文件会被安全模块拦下来表现也是读不到文件。ausearch或者journalctl能看到相关的拒绝日志。这种情况的处理方式取决于具体策略通常是在挂载时加上合适的标签或者调整策略不建议直接关掉安全模块。6.2 超时和缓冲区参数的调整时机504 和 502 不一样504 说明连上了但是等太久没等到响应。默认配置下Nginx 等 FastCGI 的读超时是 60 秒PHP 那边的max_execution_time默认是 30 秒两边其实不太匹配。location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_connect_timeout 10s; fastcgi_send_timeout 60s; fastcgi_read_timeout 120s; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; }fastcgi_connect_timeout我设成 10 秒意思是连不上就别等了早点报错比挂着强。fastcgi_read_timeout设成 120 秒是因为项目里有一些导出报表的操作确实慢PHP 那边max_execution_time也调到了 120两边对齐。如果 Nginx 这边是 60、PHP 那边是 120那实际上 60 秒就断了PHP 还在傻跑白耗资源。缓冲区那两个参数是给响应头用的。如果后端返回的响应头比较大比如带了一堆 Cookie 或者很长的 Location默认的 4k 缓冲区装不下Nginx 会报upstream sent too big header然后 502。把fastcgi_buffer_size调到 32kfastcgi_buffers调成 8 个 32k基本能覆盖绝大多数情况。这个坑我踩过一次登录接口莫名其妙 502查了两个小时最后发现是登录成功后 Set-Cookie 太多响应头超过 4k 被截断了。6.3 一个容易被忽略的点容器内 hosts 与 DNS除了 fastcgi_pass 的主机名解析还有一个地方会用到 DNS——如果 PHP 代码里要访问外部接口或者要连 MySQL它也是在容器网络里做域名解析。MySQL 容器名能不能被解析取决于它们是不是在同一个网络。这一点经常被人忽略尤其是在 PHP 连接数据库失败的时候第一反应是查账号密码实际上可能是网络就不通。排查方法很直接docker exec php ping -c 2 mysql能通就说明网络层没问题。ping 不通但docker exec php nslookup mysql能解析出 IP那就是 ICMP 被拦了不影响 TCP两个都不行那就是网络没接上回到前面检查--network参数。7. 改完配置之后怎么验证以及几种典型失效情形7.1 nginx -t 是唯一可靠的校验手段每次改完配置文件先nginx -t再 reload。这个顺序不能反也不能跳过。docker exec nginx nginx -t # nginx: the configuration file /etc/nginx/nginx.conf syntax is ok # nginx: configuration file /etc/nginx/nginx.conf test is successful docker exec nginx nginx -s reloadnginx -t会做两件事语法检查以及尝试打开配置里引用的所有文件比如 mime.types、日志文件、include 的 conf.d 文件。两个都通过才输出 successful。如果语法有问题它会明确告诉你哪个文件哪一行比如unexpected } in /etc/nginx/conf.d/default.conf:28照着改就行。我见过有人改完配置直接docker restart nginx结果配置里有个语法错误容器起不来网站直接全挂。先-t的话检查不通过容器还在正常跑着旧配置你还有时间从容修改。这个习惯能救命的。nginx -s reload是平滑重载。它会让 master 进程读取新配置、启动新的 worker、然后优雅地关掉旧 worker正在处理的请求不会中断。这个比 restart 温和得多容器重启会断开所有连接reload 不会。7.2 reload 不生效的几种真实原因改完配置 reload 了刷新浏览器发现还是老样子。这种情况我遇到过不止一次原因大致这么几类。第一类改错文件了。你以为改的是挂载进容器的那个default.conf实际上编辑器打开的是一个备份文件或者另一个路径下的同名文件。验证方式docker exec nginx cat /etc/nginx/conf.d/default.conf看容器里那份到底是什么内容。这个办法最笨也最有效能立刻排除掉改错地方的可能性。第二类浏览器缓存。尤其是改静态资源CSS、JS的时候浏览器缓存了旧版本你怎么改都看不到变化。强制刷新CtrlShiftR或者在开发者工具里勾上 Disable cache。也可以临时在 URL 后面加个随机参数试试。第三类容器内还有另一个配置文件在起作用。比如你挂载了整个conf.d目录目录里除了default.conf还有一个之前的备份default.conf.bak。虽然.bak不匹配*.conf的通配但如果你把它改名成了.conf结尾它就会被 include 进来两个 server 块都监听 80谁生效取决于加载顺序非常混乱。检查方式docker exec nginx ls /etc/nginx/conf.d/把这个目录里的文件控制在最少的必要数量。第四类reload 信号发出去了但 Nginx 没收到。nginx -s reload依赖 pid 文件如果 pid 文件路径配置不对或者容器内进程树有问题信号可能根本没送到 master 进程。稳妥的做法是用docker exec nginx nginx -t docker exec nginx nginx -s reload然后docker logs --tail 20 nginx看有没有重载日志。如果有signal process started之类的输出说明信号送到了。7.3 伪静态、gzip、缓存的配置位置要分清这三个东西经常被混着写导致要么不生效要么出现奇怪的行为。伪静态try_files是 location 级别的写在location /里。gzip 是 http 级别的写在nginx.conf的 http 块里。静态资源缓存expires是 location 级别的单独开一个 location 匹配静态文件后缀。# 放在 nginx.conf 的 http 块内 gzip on; gzip_min_length 1k; gzip_comp_level 4; gzip_types text/plain text/css application/javascript application/json image/svgxml; gzip_vary on;# 放在 default.conf 的 server 块内 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control public, max-age2592000; access_log off; }gzip_types里不要加图片和视频它们本身已经压缩过了再压一遍纯属浪费 CPU还可能让文件变大。gzip_comp_level不要设太高的值4 左右的压缩率已经不错再往上收益递减而 CPU 消耗陡增。gzip_vary on是给中间层缓存用的让它知道同一个 URL 可能返回压缩和非压缩两种内容。静态资源缓存的 location 要放在location ~ \.php$前面还是后面正则 location 是按配置文件里的书写顺序匹配的先匹配到哪个用哪个。静态资源的后缀不会命中\.php$所以顺序上其实不冲突但为了可读性我一般把静态资源放前面。8. 我在这台机器上真实踩过的几个坑8.1 挂载整个 conf.d 目录把默认页面搞没了第一次搭的时候我把~/lnmp/nginx/conf.d挂到了/etc/nginx/conf.d然后启动了容器。当时那个目录还是空的因为我打算先把容器跑起来再写配置。结果访问 8080 端口出来的是 Nginx 的默认欢迎页——这本来也没错因为镜像自带的default.conf还在。问题出在后面。我写完自己的default.conf放进目录reload页面变成了 404。查了半天才发现挂载空目录的那一刻镜像里原有的/etc/nginx/conf.d/default.conf就被目录挂载覆盖掉了容器里那个位置是空的。我的新配置生效了但 root 目录指到了/var/www/html而那个目录里当时什么文件都没有自然是 404。这个坑的本质是目录挂载是覆盖而不是合并。宿主机上目录是空的容器里对应位置原有的一切就都看不见了。理解了这一点后面的行为就都能推导出来你现在往宿主机目录里放什么容器里就只有什么。所以正确的顺序是先写好配置文件再启动容器挂载。或者先启动一个临时容器把默认配置拷出来改完再正式起。拷贝的命令是docker run --rm nginx:1.25-alpine cat /etc/nginx/conf.d/default.conf ~/lnmp/nginx/conf.d/default.conf8.2 php 容器重建之后 nginx 全站 502前面提过 DNS 解析的时机问题这里说个具体的经过。某次我改了 PHP 镜像重新构建之后docker compose up -d php容器重建了IP 变了。网站瞬间全站 502。当时第一反应是 PHP 挂了去看docker psphp 容器明明 Up 着。docker exec php php-fpm -t也没问题。后来看 nginx 的 error.log 才反应过来connect() failed while connecting to upstream, upstream: http://172.18.0.4:9000——这个 IP 是 php 容器重建之前的旧 IP新容器的 IP 已经变成.5了。nginx 在启动时把php解析成了.4并缓存下来php 容器重建了它也不知道。处理办法就是docker restart nginx让它重新解析一次。这件事之后我把 compose 的启动顺序改成了先 php 后 nginx并且在改动 php 容器的操作之后习惯性加一条 nginx 重启。虽然有点笨但比全站挂掉强。如果不想每次都重启 nginx前面说的resolver加变量方案是正解代价是配置复杂一点。这里的取舍就看你的环境变更频率了我这种改一次跑很久的场景重启更省事。8.3 日志目录的属主问题与磁盘占满最后一个坑是日志。挂载~/lnmp/nginx/logs到/var/log/nginx之后容器启动日志写不进去error.log 里报open() /var/log/nginx/error.log failed (13: Permission denied)。原因是宿主机的logs目录是我用普通用户创建的属主是当前用户而容器里的 nginx worker 进程以 nginx 用户通常 UID 101运行对那个目录没有写权限。解决办法要么把宿主机目录的权限放宽一点chmod 777虽然能解决但不推荐要么把目录属主改成容器内 nginx 用户的 UIDsudo chown -R 101:101 ~/lnmp/nginx/logsUID 101 是官方 alpine 镜像里 nginx 用户的固定 ID可以先docker exec nginx id nginx确认一下。改完之后日志就能正常写了。另一个更麻烦的情况是日志一直写磁盘满了。Nginx 默认不切割日志访问量上来之后access.log会一直涨。容器环境里常规的 logrotate 用不上我的做法是在nginx.conf里把访问日志格式简化同时配合 Docker 的日志驱动限制容器 stdout 的大小另外加一条定时任务定期清空或者归档日志文件。测试环境更简单粗暴直接truncate -s 0清空但要注意 Nginx 持有的是文件句柄清空之后新写入会从文件开头继续不会错位这个跟rm掉文件的行为不一样——rm掉之后 Nginx 还在往那个已经不存在的 inode 写磁盘空间不会释放必须 reload 才能重新打开。# 清空而不删除磁盘空间立即释放Nginx 无需 reload truncate -s 0 ~/lnmp/nginx/logs/access.log最后再分享一个小技巧给 nginx 容器加一个 healthcheck用wget或者curl打一下本地 80 端口失败就标记为 unhealthy。配合 compose 的depends_on的condition: service_healthy能让上层服务在 nginx 真正可用之后再启动。这个在自动化部署里挺有用省得服务起来了但入口还没准备好。配置大概是这样healthcheck: test: [CMD, wget, -q, --spider, http://127.0.0.1/] interval: 30s timeout: 5s retries: 3 start_period: 10sstart_period这个参数是给容器启动留的缓冲时间在这段时间内的检查失败不计入重试次数避免容器刚起来还在初始化就被判定为不健康。设成 10 秒对我这个场景够用如果你的项目启动时要加载很多东西适当调大一点。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询