ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

nginx-1.24.0 Docker镜像自构建指南:模块定制与避坑实践

2026/10/8 9:57:13 拓冰建站 浏览量
nginx-1.24.0 Docker镜像自构建指南:模块定制与避坑实践 简介这份资源是 nginx-1.24.0 的 Docker 镜像构建素材包面向需要自建镜像的运维与后端开发人员尤其适合希望摆脱官方镜像黑盒、按需裁剪或定制编译参数的场景。包内共 48 个文件以 23 个 sh 脚本和 11 个 Dockerfile 为主辅以 5 个 template 模板、3 个 md 说明文档及 license、gitignore 等配置压缩包仅 64KB轻量易取。脚本覆盖入口初始化、环境变量替换、IPv6 监听、worker 进程调优等环节Dockerfile 则按 debian、alpine、perl、slim 等基础镜像与变体分列并配有模板文件与生成脚本便于批量维护多版本构建。读者可据此理解官方镜像的分层组织方式掌握从基础镜像选择、依赖安装到入口脚本编排的完整链路进而产出体积更小、行为可控的私有 nginx 镜像。目前已有 953 人学习下载适合具备一定 Docker 基础、想深入镜像构建细节的开发者参考。1. nginx-1.24.0 docker镜像为什么我宁愿自己构建也不直接 pull上周帮一个朋友排查线上问题现象很典型docker pull nginx:1.24.0拉下来的镜像跑起来之后nginx -V显示的编译参数里没有--with-http_realip_module导致他前面挂了一层负载均衡之后后端服务拿到的全是 LB 的内网 IP业务日志里的客户端 IP 全丢了。他以为是配置写错了折腾了一下午set_real_ip_from最后发现是镜像本身没编进去这个模块。这件事让我再次确认一个判断nginx-1.24.0这个版本号对应的 docker 镜像官方 Docker Hub 上确实有但它是一个「通用够用」的构建不是你生产环境「刚好够用」的构建。nginx 1.24.0 是 2023 年 4 月发布的稳定版主分支版本修了 1.23.x 的一堆问题很多公司的基线就卡在这个版本上。但官方镜像默认只带了最常见的那些模块--with-http_ssl_module、--with-http_v2_module、--with-http_gzip_static_module这些有但realip、sub、stub_status、http_auth_request这些就得看运气。所以这篇不是教你「怎么 docker run nginx」而是把 nginx-1.24.0 这个特定版本的镜像从「拿到手」到「改造成自己能用的」整条链路拆开。适合两类人一是需要固定 nginx 版本做基线、又不想被官方镜像的编译参数绑架的运维二是本地开发环境想用 docker 跑多站点、自定义域名但被镜像里的默认配置坑过的后端。下面从镜像本身的结构讲起再落到构建、配置、排错。2. nginx-1.24.0 镜像的构成与构建选型从 pull 到自建的决策链2.1 官方镜像里到底装了什么先把nginx:1.24.0这个 tag 对应的镜像拆开看。它基于 Debian 12bookworm的 slim 变体镜像里 nginx 的安装路径是/usr/sbin/nginx配置目录/etc/nginx/默认站点根目录/usr/share/nginx/html日志走 stdout/stderr 的重定向/var/log/nginx/access.log软链到/dev/stdout。这些是官方镜像的约定不是 nginx 本身的约定很多人第一次用会懵为什么我改了/etc/nginx/nginx.conf里的access_log路径日志还是打到 docker logs 里去了因为官方镜像在/etc/nginx/nginx.conf里写了access_log /var/log/nginx/access.log main;而那个文件是个软链。关键在编译参数。官方镜像的 nginx 是用nginx:1.24.0的 Dockerfile 构建的里面./configure那一行大致是./configure \ --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 \ --pid-path/var/run/nginx.pid \ --lock-path/var/run/nginx.lock \ --http-client-body-temp-path/var/cache/nginx/client_temp \ --http-proxy-temp-path/var/cache/nginx/proxy_temp \ --http-fastcgi-temp-path/var/cache/nginx/fastcgi_temp \ --http-uwsgi-temp-path/var/cache/nginx/uwsgi_temp \ --http-scgi-temp-path/var/cache/nginx/scgi_temp \ --usernginx \ --groupnginx \ --with-compat \ --with-file-aio \ --with-threads \ --with-http_addition_module \ --with-http_auth_request_module \ --with-http_dav_module \ --with-http_flv_module \ --with-http_gunzip_module \ --with-http_gzip_static_module \ --with-http_mp4_module \ --with-http_random_index_module \ --with-http_realip_module \ --with-http_secure_link_module \ --with-http_slice_module \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_sub_module \ --with-http_v2_module \ --with-mail \ --with-mail_ssl_module \ --with-stream \ --with-stream_realip_module \ --with-stream_ssl_module \ --with-stream_ssl_preread_module注意这是官方镜像的构建参数--with-http_realip_module其实是有的。那为什么我朋友那个镜像没有因为他用的不是官方镜像是某个第三方仓库的nginx:1.24.0那个仓库为了减小体积把realip和stub_status都裁了。这就是第一个坑同名 tag 不等于同一份构建。Docker Hub 上nginx是官方但如果你从别的 registry 或者公司内部 harbor 拉tag 一样内容可能完全不同。验证方法很简单跑一个临时容器docker run --rm nginx:1.24.0 nginx -V 21 | tr \n | grep with这条命令把nginx -V的输出按空格拆行过滤出所有--with-开头的编译参数。21是因为nginx -V把版本信息打到 stderr 而不是 stdout不加这个重定向你什么都看不到。跑完你就能确认这个镜像到底带了哪些模块再决定要不要自己构建。2.2 什么时候该自己构建什么时候直接用判断标准不是「官方镜像好不好」而是「你的需求是否落在官方编译参数的补集里」。我一般按下面这张表决策场景建议理由纯静态站点、简单反向代理直接用官方nginx:1.24.0官方参数覆盖了 ssl、v2、gzip_static够用需要realip拿真实客户端 IP先nginx -V确认没有就自建第三方镜像常裁这个模块需要stub_status做监控先确认没有就自建zabbix/ Prometheus 采集 nginx 指标依赖它需要 Lua、njs 等第三方模块必须自建官方镜像不带需要固定 glibc 版本、走内部基础镜像必须自建官方镜像基于 Debian你的基线可能是 Alpine 或 UBI本地开发多站点、自定义域名官方镜像 挂载配置即可不需要改编译参数自建的成本没有想象中高。nginx 1.24.0 的源码包不到 1.2MB编译依赖gcc、make、libpcre3-dev、zlib1g-dev、libssl-dev这几个在 Debian 基础镜像里apt-get install一把就齐了。真正花时间的是把编译参数调对以及处理--with-compat动态模块的加载路径。2.3 一份可复现的 Dockerfile下面这份 Dockerfile 是我在用的基于 Debian 12 slim编译 nginx 1.24.0保留官方那套参数额外加上--with-http_realip_module和--with-http_stub_status_module其实官方也有但显式写出来防止基础镜像差异并且把 nginx 用户和目录结构对齐官方镜像这样配置迁移成本最低。FROM debian:12-slim AS builder ARG NGINX_VERSION1.24.0 RUN apt-get update apt-get install -y --no-install-recommends \ gcc make libpcre3-dev zlib1g-dev libssl-dev ca-certificates \ rm -rf /var/lib/apt/lists/* WORKDIR /usr/src RUN curl -fsSL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -o nginx.tar.gz \ tar -xzf nginx.tar.gz WORKDIR /usr/src/nginx-${NGINX_VERSION} RUN ./configure \ --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 \ --pid-path/var/run/nginx.pid \ --lock-path/var/run/nginx.lock \ --http-client-body-temp-path/var/cache/nginx/client_temp \ --http-proxy-temp-path/var/cache/nginx/proxy_temp \ --http-fastcgi-temp-path/var/cache/nginx/fastcgi_temp \ --http-uwsgi-temp-path/var/cache/nginx/uwsgi_temp \ --http-scgi-temp-path/var/cache/nginx/scgi_temp \ --usernginx \ --groupnginx \ --with-compat \ --with-file-aio \ --with-threads \ --with-http_addition_module \ --with-http_auth_request_module \ --with-http_dav_module \ --with-http_flv_module \ --with-http_gunzip_module \ --with-http_gzip_static_module \ --with-http_mp4_module \ --with-http_random_index_module \ --with-http_realip_module \ --with-http_secure_link_module \ --with-http_slice_module \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_sub_module \ --with-http_v2_module \ --with-stream \ --with-stream_realip_module \ --with-stream_ssl_module \ --with-stream_ssl_preread_module \ make -j$(nproc) \ make install FROM debian:12-slim RUN apt-get update apt-get install -y --no-install-recommends \ libpcre3 zlib1g libssl3 ca-certificates \ rm -rf /var/lib/apt/lists/* \ groupadd -r nginx useradd -r -g nginx -s /sbin/nologin nginx COPY --frombuilder /usr/sbin/nginx /usr/sbin/nginx COPY --frombuilder /etc/nginx /etc/nginx COPY --frombuilder /usr/lib/nginx /usr/lib/nginx RUN mkdir -p /var/cache/nginx /var/log/nginx \ chown -R nginx:nginx /var/cache/nginx /var/log/nginx EXPOSE 80 443 STOPSIGNAL SIGQUIT CMD [nginx, -g, daemon off;]这份 Dockerfile 分两阶段。builder 阶段装编译工具链、下载源码、configure、make、make install。final 阶段只装运行时依赖libpcre3、zlib1g、libssl3把编译产物拷过来建 nginx 用户建缓存和日志目录。这样最终镜像不会带上 gcc 和源码体积能控制在 60MB 左右。几个参数说明。--with-compat是为了让动态模块的 ABI 兼容如果你以后要加第三方动态模块这个必须开。--with-threads开了线程池高并发静态文件场景有用。--with-file-aio在 Linux 上开异步 IO但注意它在某些文件系统上反而拖慢容器里如果挂的是 overlayfs实测收益不明显可以按需去掉。STOPSIGNAL SIGQUIT是让 nginx 收到docker stop时走优雅退出而不是被 SIGTERM 直接砍掉这个细节在滚动更新时很关键。构建命令docker build -t my-nginx:1.24.0 .构建完同样用nginx -V验证一遍确认realip和stub_status都在。这一步别省我见过 configure 参数写对了但 make 阶段因为缺依赖静默跳过某个模块的情况虽然少见但验证一次只要两秒。3. 用这份镜像跑多站点与反向代理配置挂载与 location 工作流3.1 配置目录的挂载策略拿到镜像之后第一件事是决定配置怎么进去。有三种做法一是COPY进镜像二是-v挂载宿主机目录三是用 ConfigMapk8s 场景。本地开发和测试环境我推荐挂载因为改完nginx -s reload就生效不用重新构建镜像。挂载的时候有个细节官方镜像的/etc/nginx/conf.d/目录是给站点配置用的nginx.conf里有一行include /etc/nginx/conf.d/*.conf;。你挂载的时候如果只挂conf.d那nginx.conf还是镜像里的默认版本这通常够用。但如果你要改nginx.conf本身比如调worker_processes、worker_connections就得把整个/etc/nginx挂进去这时候要注意别把镜像里的mime.types覆盖丢了否则所有静态文件的 Content-Type 都会变成application/octet-stream浏览器下载而不是渲染。我一般这么做docker run -d --name nginx-dev \ -p 8080:80 \ -v $(pwd)/conf.d:/etc/nginx/conf.d:ro \ -v $(pwd)/html:/usr/share/nginx/html:ro \ -v $(pwd)/logs:/var/log/nginx \ my-nginx:1.24.0conf.d和html用:ro只读挂载防止容器内进程误写。logs目录可写方便在宿主机直接tail -f。端口映射8080:80是因为宿主机 80 端口通常被占本地开发用 8080 起步多站点就 8080、8081 依次排。3.2 多站点配置与自定义域名本地开发想用site-a.local、site-b.local这种自定义域名访问核心是server_name加宿主机 hosts。假设你有两个站点配置分别放在conf.d/site-a.conf和conf.d/site-b.conf# conf.d/site-a.conf server { listen 80; server_name site-a.local; root /usr/share/nginx/html/site-a; index index.html; location / { try_files $uri $uri/ 404; } location /api/ { proxy_pass http://host.docker.internal:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }# conf.d/site-b.conf server { listen 80; server_name site-b.local; root /usr/share/nginx/html/site-b; index index.html; location / { try_files $uri $uri/ 404; } }server_name匹配的是 HTTP 请求头里的 Host 字段所以宿主机/etc/hostsWindows 是C:\Windows\System32\drivers\etc\hosts里要加127.0.0.1 site-a.local 127.0.0.1 site-b.local这样浏览器访问http://site-a.local:8080时请求打到宿主机的 8080docker 转发到容器 80nginx 根据 Host 头匹配到site-a的 server 块。注意端口要带上因为 hosts 文件只解析域名到 IP不解析端口。proxy_pass http://host.docker.internal:3000/里的host.docker.internal是 Docker Desktop 提供的特殊域名指向宿主机。Linux 上原生 docker 没有这个域名需要加--add-hosthost.docker.internal:host-gateway参数或者直接用宿主机的局域网 IP。这个坑在「本地容器连宿主机服务」时几乎人人踩一次。3.3 location 匹配的工作流机制多站点配置里最容易出玄学的就是location。nginx 的 location 匹配不是「从上到下第一个匹配就停」而是一套有优先级的规则。理解这套规则能省掉大量「为什么我的配置没生效」的排查时间。匹配顺序大致是精确匹配命中即停优先级最高。^~前缀匹配命中后不再检查正则。~和~*正则匹配按配置文件里出现的顺序第一个命中即停。普通前缀匹配记住最长匹配的那个。如果最长前缀匹配的 location 带^~用它的结果否则继续看正则有没有命中正则命中就用正则的没命中才用最长前缀的。举个例子location / { return 200 exact root\n; } location ^~ /static/ { root /usr/share/nginx/html; } location ~* \.(jpg|png|css|js)$ { expires 7d; add_header Cache-Control public; } location / { proxy_pass http://backend; }访问/走精确匹配返回exact root。访问/static/logo.png^~ /static/命中后直接停不会再去看~* \.(jpg|png...)$那条正则所以静态资源不会带上expires 7d。如果你希望静态资源带缓存头就得把expires写进^~ /static/块里或者去掉^~让它走正则。这就是「location 工作流机制」的实际影响^~是一道闸过了它后面的正则全部失效。访问/api/user^~ /static/不匹配正则\.(jpg|png...)$也不匹配最后落到location /走代理。访问/logo.png正则命中走缓存头不走代理。这套规则记不住没关系排查的时候用nginx -T把最终配置打出来再对照请求路径手动推一遍比猜快得多。4. 镜像使用中的避坑与排查五个真实翻车记录4.1 现象容器启动后立刻退出docker logs只有一行nginx: [emerg] ...原因通常是配置文件语法错误或者挂载的目录不存在导致include失败。nginx 在daemon off模式下配置解析失败就直接退出不会留在前台。解决先用docker run --rm my-nginx:1.24.0 nginx -t做语法检查。注意-t只检查语法不检查路径是否存在include的文件不存在会报错但root指向的目录不存在不会。如果-t过了还是退出看docker logs里的完整错误行通常是open() /etc/nginx/conf.d/xxx.conf failed (2: No such file or directory)这种说明挂载路径写错了。挂载宿主机目录时如果宿主机目录不存在docker 会自动创建一个空目录include *.conf匹配不到文件不报错但如果你挂的是单个文件而文件不存在docker 会创建一个同名目录nginx 读目录就报错。这个行为很坑挂单文件前先确认文件存在。4.2 现象docker stop要等 10 秒才停或者直接 SIGKILL原因是没有正确处理STOPSIGNAL。官方镜像设了STOPSIGNAL SIGQUITnginx 收到 SIGQUIT 会走优雅退出等当前请求处理完。但如果你自己构建时忘了这行默认是 SIGTERMnginx 对 SIGTERM 的处理是快速退出正在处理的请求会被中断。反过来如果docker stop等 10 秒还没停说明有长连接没断nginx 在等 worker 退出。解决Dockerfile 里加STOPSIGNAL SIGQUIT。如果已经跑起来了docker stop -t 30给足超时时间。另外检查worker_shutdown_timeout配置默认是无限等设一个合理值比如worker_shutdown_timeout 10s;能避免无限挂起。4.3 现象反向代理后后端拿到的客户端 IP 全是容器网段地址原因是没配realip模块或者配了但set_real_ip_from的网段不对。nginx 作为代理时$remote_addr是上一跳的地址也就是负载均衡器或 docker 网关的地址。要拿到真实客户端 IP需要信任的代理把 IP 写进X-Forwarded-For或X-Real-IPnginx 用realip模块从这些头里取。解决确认镜像带了--with-http_realip_module用nginx -V查。然后在server或http块里配set_real_ip_from 172.16.0.0/12; set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For; real_ip_recursive on;set_real_ip_from填你信任的代理网段docker 默认网桥是172.17.0.0/16自定义网络可能是172.18到172.31所以172.16.0.0/12覆盖了这些。real_ip_recursive on表示从X-Forwarded-For最右边开始跳过所有信任网段的地址取第一个不信任的作为真实 IP。如果不开这个nginx 直接取最右边的地址多级代理时会取错。4.4 现象挂载nginx.conf后静态文件全部变成下载原因是覆盖了/etc/nginx/nginx.conf但新文件里没有include /etc/nginx/mime.types;或者mime.types文件没挂进去。nginx 不知道.css、.js、.png对应什么 Content-Type默认给application/octet-stream浏览器就触发下载。解决检查nginx.conf的http块里有没有include mime.types;和default_type application/octet-stream;。如果挂载了整个/etc/nginx目录确认mime.types文件在挂载目录里。最省事的做法是只挂conf.d不动nginx.conf这样mime.types始终用镜像里的。4.5 现象proxy_pass带路径时后端收到的路径不对原因是proxy_pass末尾有没有斜杠行为完全不同。proxy_pass http://backend;会把原始 URI 原样转发proxy_pass http://backend/;会把 location 匹配到的前缀替换成/。举例location /api/ { proxy_pass http://backend/; }请求/api/user转发到后端是/user。如果写成proxy_pass http://backend;转发到后端是/api/user。这个差异在配置多级路径时极易搞混而且 nginx 不会报错只是后端 404。解决记住一条规则——proxy_pass的 URL 带路径哪怕只是一个/就会做路径替换不带路径只有 host 和端口就原样透传。排查时在后端打日志看实际收到的 path比对着配置猜快。5. 进阶用 stub_status 和日志格式把 nginx 变成可观测的镜像跑起来只是第一步真正让它在生产里站住脚的是可观测性。nginx 1.24.0 自带的stub_status模块能暴露连接数、请求数这些基础指标配合自定义日志格式能把「玄学问题」变成「有数据可查」。先开stub_status。在某个server块里加server { listen 8080; server_name localhost; location /nginx_status { stub_status; allow 127.0.0.1; allow 172.16.0.0/12; deny all; } }stub_status输出的内容长这样Active connections: 3 server accepts handled requests 120 120 350 Reading: 0 Writing: 1 Waiting: 2Active connections是当前活跃连接数accepts是总接受连接数handled是成功处理数requests是总请求数。Reading是正在读请求头的连接Writing是正在写响应的Waiting是 keepalive 空闲连接。如果Waiting持续很高说明 keepalive 连接堆积可以调keepalive_timeout。如果accepts和handled不相等说明有连接被丢弃通常是worker_connections不够或者文件描述符限制。然后是日志格式。默认的combined格式只有$remote_addr、$request、$status、$body_bytes_sent、$http_referer、$http_user_agent。排查性能问题时这些不够。我一般定义一个main格式加上$request_time、$upstream_response_time、$upstream_addrlog_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for rt$request_time urt$upstream_response_time ua$upstream_addr; access_log /var/log/nginx/access.log main;$request_time是 nginx 从收到请求第一个字节到发送完响应最后一个字节的总耗时$upstream_response_time是后端响应耗时。如果rt很大但urt很小说明瓶颈在 nginx 本身或者客户端网络如果urt接近rt说明瓶颈在后端。$upstream_addr记录实际转发到的后端地址多后端负载时能看出请求打到了哪台。这两个配置加完docker exec进容器跑nginx -s reload然后curl http://localhost:8080/nginx_status验证。注意stub_status的allow要限制来源别暴露到公网否则连接数信息会被外部看到。最后说一个我自己的习惯。每次构建完 nginx 镜像我会跑一个三行的验证脚本nginx -V确认模块nginx -t确认配置语法curl -I localhost确认服务能响应。这三步走完镜像才算「可用」。从那以后我每次换基础镜像或者改编译参数都强制走一遍这个流程省掉了很多「构建成功但跑不起来」的后悔药。希望帮到你。本文还有配套的精品资源点击获取