简介:这份资源是 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 12(bookworm)的 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 \ --user=nginx \ --group=nginx \ --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 2>&1 | tr ' ' '\n' | grep with这条命令把nginx -V的输出按空格拆行,过滤出所有--with-开头的编译参数。2>&1是因为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_VERSION=1.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 \ --user=nginx \ --group=nginx \ --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 --from=builder /usr/sbin/nginx /usr/sbin/nginx COPY --from=builder /etc/nginx /etc/nginx COPY --from=builder /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挂载宿主机目录,三是用 ConfigMap(k8s 场景)。本地开发和测试环境我推荐挂载,因为改完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/hosts(Windows 是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时,请求打到宿主机的 8080,docker 转发到容器 80,nginx 根据 Host 头匹配到site-a的 server 块。注意端口要带上,因为 hosts 文件只解析域名到 IP,不解析端口。
proxy_pass http://host.docker.internal:3000/里的host.docker.internal是 Docker Desktop 提供的特殊域名,指向宿主机。Linux 上原生 docker 没有这个域名,需要加--add-host=host.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 SIGQUIT,nginx 收到 SIGQUIT 会走优雅退出,等当前请求处理完。但如果你自己构建时忘了这行,默认是 SIGTERM,nginx 对 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-IP,nginx 用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_addr:
log_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确认服务能响应。这三步走完,镜像才算「可用」。从那以后我每次换基础镜像或者改编译参数,都强制走一遍这个流程,省掉了很多「构建成功但跑不起来」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取