资讯动态

Ubuntu自建Nginx镜像:Dockerfile设计、瘦身与排错

发布时间:2026/10/27 21:44:14 来源:尧图企业网站定制
容器化部署这块我踩过最蠢的一个坑是早年间直接docker pull nginx就上线结果某次线上要加一个第三方模块、要统一时区、要把访问日志格式改成能对接内部统计系统的样子才发现官方镜像里那些看不见的约定全都得推翻重来。后来我把基础镜像统一换成了自己维护的构建在 Ubuntu 系统上运行的 Nginx 镜像从此配置、依赖、目录结构、退出信号全都握在自己手里。这篇就把这套东西从设计思路到 Dockerfile 逐层拆解、再到实操验证和排错完整讲一遍。不管你是刚接触容器、还在照着教程敲docker run的新手还是已经在维护一批自建镜像、天天跟构建缓存和体积较劲的老手下面这些取舍逻辑和踩坑记录应该都能对上你的场景。1. 需求拆解与整体设计思路动手写 Dockerfile 之前先得把为什么要自己构建这件事想明白。很多人第一步就走偏了拿到需求就开始搜Dockerfile nginx 教程复制一份能跑起来就完事结果三个月后要升级版本、要加模块、要换时区发现当初那份文件根本没法改。我一般是先把需求列成三条线功能线跑什么、要哪些模块、运维线日志、信号、健康检查、时区、安全线用户、权限、体积、漏洞面。三条线都想清楚了Dockerfile 自然就长出来了而且是能长期维护的那种。1.1 官方镜像在什么场景下会让人不踏实官方 Nginx 镜像本身质量很高绝大多数场景直接用它完全没问题我从不劝人无脑自建。真正让人不踏实的是那些官方镜像替你做主、但你的环境不认的地方。第一是基础系统官方镜像的主线基于 Debian-slim和-alpine各有各的取舍而很多内部环境的安全基线、合规扫描、运维脚本全是按 Ubuntu 体系写的包管理习惯、目录约定、systemd 之外的初始化方式都对不上这时候硬用 Debian 底座排查问题时两边文档来回翻效率极低。第二是软件来源和版本锁定。官方镜像里 Nginx 是编译安装的路径是/usr/sbin/nginx、配置在/etc/nginx这些都没问题但你要加一个非默认模块就得自己从源码编译那时候官方镜像的那套编译参数你未必能完整复现。第三是时间与语言环境官方镜像默认 UTC、默认 C locale日志时间戳跟业务侧对不上中文文件名或中文错误信息偶尔会出乱码虽然能通过挂载和环境变量绕过去但每次部署都要绕一遍早晚出事。第四点最隐蔽镜像里的默认配置和你的运维规范冲突。比如默认client_max_body_size 1m业务一传图就 413默认access_log写文件容器日志里啥也看不到默认daemon on容器起来就退出。这些都不是缺陷是默认值选择问题但每一条都会在某个深夜把你叫起来。自己构建一版把这些默认值一次性定死比每次部署时打补丁靠谱得多。1.2 基础镜像选型ubuntu:22.04 还是 alpine、debian这是第一个必须做决策的点我把常见的几个选项拉出来对比过很多次结论是没有绝对最优只有跟你的运维体系最匹配。下面这张表是我自己整理的经验值体积是构建完成后docker images看到的量级不同时间拉取会略有浮动。基础镜像最终镜像体积量级包管理与生态适合的场景主要代价ubuntu:22.04约 130–150 MBapt与主流 Linux 运维习惯一致内部规范基于 Ubuntu、需要 apt 装依赖、需要 glibc体积偏大需要主动清理缓存debian:bookworm-slim约 100–120 MBapt与 Ubuntu 基本同源想要 apt 但更在意体积部分包的版本比 Ubuntu 保守alpine:3.19约 45–60 MBapkmusl libc极致瘦身、静态资源托管、边缘节点musl 兼容性坑某些第三方模块编译不过官方 nginx:alpine约 45 MB官方预编译无定制需求、快速起服务定制能力弱底座不是你的体系我最终选ubuntu:22.04的原因很朴素团队里所有人的排错肌肉记忆都是 Ubuntu 的——apt、/etc/nginx、/var/log/nginx、systemctl之外的进程管理方式全都是同一套。选 alpine 能省 80MB但一旦碰上 musl 编译问题排查成本远超这 80MB 带来的收益。当然如果你的场景就是纯静态资源托管、不需要任何额外模块、节点数量上千那 alpine 的体积优势是实打实的这时候选它完全正确。还有一个容易被忽略的点基础镜像的 tag 要写死到具体版本。写FROM ubuntu:22.04是可以的因为 22.04 是个长期支持版本补丁更新通常向后兼容但如果你写FROM ubuntu:latest某天上游一升级你的构建可能直接挂掉而且你完全不知道是哪次变更导致的。我这边所有 Dockerfile 的 FROM 行都要求带明确版本号这是硬规矩。1.3 构建方式为什么坚持 Dockerfile 而不是 docker commit新手最容易走的捷径是起一个 Ubuntu 容器进去apt install nginx改改配置然后docker commit存成镜像。这条路十分钟能出结果但我强烈不建议用在任何需要长期维护的东西上。原因有三个一是不可复现commit 出来的镜像没有构建历史你不知道里面到底装了什么、改了哪些文件下次要升级只能重新手动来一遍二是不可审计安全扫描看到一个几百 MB 的黑盒镜像根本没法判断风险点在哪三是没法做缓存优化和分层设计commit 出来的层是乱的后续任何改动都会让整个镜像重新传输。Dockerfile 的价值不在能构建出镜像而在于它是一份可执行的文档。任何人拿到这个文件都能在任意一台装了 Docker 的机器上复现出一模一样的镜像diff 一下就知道这次改了什么。我维护的镜像仓库里每个镜像都必须有对应的 Dockerfile 入库commit 出来的镜像一律打回重做。这个规矩定下来之后线上这个镜像到底怎么来的这类问题基本绝迹了。至于单阶段还是多阶段要看你的安装方式。如果是用apt装预编译的 Nginx 包单阶段就够了多阶段反而增加复杂度——因为 apt 装出来的东西本身就是运行时需要的没有编译期依赖和运行期依赖的明显区分。只有当你需要从源码编译 Nginx、要加第三方模块比如某些图像处理、认证、限流模块时多阶段才有意义第一阶段装编译器、源码、开发库编译出二进制第二阶段只拷二进制和运行库。这个区别很关键很多人看教程看到多阶段就照抄结果发现自己根本没用上。1.4 分层设计与缓存策略的取舍Docker 镜像是一层一层叠起来的每一层对应 Dockerfile 里的一条指令。理解这一点后面所有的优化才有依据。核心规则只有一条任何一条 RUN 指令产生的文件如果在同一条 RUN 里删掉才不会进入最终镜像如果分成两条 RUN第一条装的东西在第二条里删掉体积一点都不会减小。这是新手最常犯的错误也是我见过最多的我明明清理了缓存为什么镜像还是这么大。基于这条规则我的分层设计原则是这样的变化频率低的东西放前面变化频率高的放后面。基础镜像、系统包安装、时区设置这些一年改不了一次的放最前面Nginx 版本号这种半年改一次的放中间配置文件、静态资源这种天天改的放最后。这样日常改个配置只需要重新构建最后一两层前面的全部命中缓存构建时间从几分钟降到几秒。另外还有两个必须注意的点。一是apt-get update和apt-get install必须在同一条 RUN里分开写的话update那一层会被缓存等你下次加新包的时候安装用的是几天前的包索引很可能报Unable to locate package。二是COPY指令的顺序要精心安排先拷贝不常变的配置文件再拷贝经常变的静态资源目录否则每次改个 HTML 都会让后面的所有层缓存失效。2. Dockerfile 核心细节逐条拆解Dockerfile 写起来就那么十几行但每一行的写法都有讲究写错了当时能跑埋的雷可能几个月后才炸。这一章我按执行的先后顺序把关键几行的设计意图、常见错误写法、以及为什么这么写逐条讲清楚。2.1 apt 层一行搞定 update、install 与清理系统包安装是整个构建里最容易写臃肿的一层。先看一个反面例子我见过的真实写法RUN apt-get update RUN apt-get install -y curl gnupg tzdata RUN rm -rf /var/lib/apt/lists/*这三行分开写结果是第一行产生了一层包含完整包索引的层大约 20–30MB第二行产生了安装包的层第三行虽然删了索引但只是在新的一层里打了个删除标记第一层里的索引文件依然存在于镜像里。最终镜像白胖了二三十兆而且你还以为自己清理过了。正确的写法是把所有操作压缩到一条 RUN 里用串起来并且用set -eux开头保证任何一步失败就整体失败而不是默默跳过继续往下走最后构建出一个残缺镜像还不知道RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ ca-certificates curl gnupg tzdata; \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \ echo $TZ /etc/timezone; \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*这里有几个参数值得单独说。--no-install-recommends的作用是不装推荐包Ubuntu 的包依赖里有 recommended 这一档不装它可能连带少装十几个非必需的包体积能省下不少代价是某些功能要自己确认。set -eux里的e是遇错退出u是使用未定义变量时报错x是打印执行的命令——构建失败时能直接从日志里看出是哪一步炸的排查效率差别巨大。tzdata这个包必须显式装因为它是交互式的安装过程中会弹时区选择界面如果不装它、或者不设DEBIAN_FRONTENDnoninteractive构建会卡在那里等输入表现就是构建卡住了。所以 ENV 里那句DEBIAN_FRONTENDnoninteractive不是可选项是必须的。时区的设置方式也有讲究ln -snf建软链是标准做法同时往/etc/timezone写一份这样date命令和某些依赖/etc/timezone的程序都能拿到正确值。注意清理缓存的动作一定要和安装动作在同一个 RUN 里跨 RUN 删除不会减小镜像体积。这条规则的例外只有一个——用 BuildKit 的 cache mount那是另一套机制后面会讲。2.2 Nginx 从哪来系统源与官方源的取舍Ubuntu 自带的软件源里是有 nginx 的apt install nginx就能装但版本通常比上游落后一两个大版本。如果你的需求就是能跑,系统源完全够用而且省去了配源的麻烦。但如果需要特定版本、或者要用到新版本才有的特性就得换成 Nginx 官方维护的 Ubuntu 软件源。配官方源这一步现在网上大量流传的还是apt-key add的写法但那个方式在新版本里已经废弃了会报 warning 甚至在未来的版本里直接失效。正确做法是把公钥转成 keyring 文件然后用signed-by指向它RUN set -eux; \ curl -fsSL https://nginx.org/keys/nginx_signing.key \ | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg; \ echo deb [signed-by/usr/share/keyrings/nginx-archive-keyring.gpg] \ http://nginx.org/packages/ubuntu jammy nginx \ /etc/apt/sources.list.d/nginx.listgpg --dearmor的作用是把 ASCII 格式的公钥转成二进制格式apt 只认二进制。-fsSL那几个参数是 curl 的标准安全写法f是 HTTP 错误时返回非零退出码否则curl拿到一个 404 页面也照样返回成功公钥就变成了一段 HTML后面全崩s静默S显示错误L跟随重定向。这里有个坑必须提醒源里的代号必须和基础镜像匹配。ubuntu:22.04的代号是jammy如果你写成了focal20.04或者noble24.04apt 要么找不到包要么装上了但依赖关系错乱。对应的仓库地址路径是http://nginx.org/packages/ubuntu加上发行代号和组件名nginx这三段缺一不可。装的时候建议把版本号锁死用ARG传进来ARG NGINX_VERSION1.24.0-1~jammy ARG TZAsia/Shanghai ... apt-get install -y --no-install-recommends nginx${NGINX_VERSION};版本号的具体格式不要凭记忆写构建前先跑一句docker run --rm ubuntu:22.04 bash -c apt-get update apt-cache madison nginx看一眼实际可用的版本清单格式通常是主版本.次版本.补丁-发布号~代号。这一步花三十秒能省掉后面半小时的Unable to locate package排查。锁版本的好处是可复现坏处是每次升级要改文件但这个改动是有意识的、可审查的比某天自动变成了新版本然后行为不一致要好得多。还有一个体积上的技巧gnupg只在拉公钥那一步需要装完就可以清掉。但注意清理必须写在同一个 RUN 里如果单独开一条RUN apt-get purge -y gnupg体积不降反增。写在同一条 RUN 里的写法是apt-get purge -y --auto-remove gnupg注意--auto-remove会把它带进来的依赖也一起清掉。如果清理之后发现 apt 源校验报错说明某些校验工具被误删了把那一步去掉、保留 gnupg 就行几兆的体积不值得冒这个风险。2.3 日志与信号让容器日志和优雅退出都正常这两个东西是容器化 Nginx 最容易出问题、也最少被讲清楚的地方。先说日志。Nginx 默认把访问日志写到/var/log/nginx/access.log错误日志写到同目录的error.log。在物理机上这没问题但在容器里这些文件的内容不会出现在docker logs的输出里你只能docker exec进去tail非常别扭而且日志文件会随着时间不断增长最终把容器写满。标准解法是把这两个文件路径软链到标准输出和标准错误RUN ln -sf /dev/stdout /var/log/nginx/access.log \ ln -sf /dev/stderr /var/log/nginx/error.log这样 Nginx 以为自己还在写文件实际写进了容器的 stdout/stderrdocker logs、日志采集组件全都能正常拿到。为什么访问日志走 stdout、错误日志走 stderr因为这是 Unix 的约定正常输出和异常输出分流日志采集侧可以按流做不同的处理策略比如错误日志直接触发告警。再说信号。Nginx 的 master 进程对信号的处理是SIGTERM是快速关闭SIGQUIT是优雅关闭等当前请求处理完再退出SIGHUP是重新加载配置SIGUSR1是重新打开日志文件。Docker 停止容器时默认发SIGTERM如果不做处理Nginx 会直接断掉正在处理的请求用户看到的就是连接被重置。所以 Dockerfile 里要加一行STOPSIGNAL SIGQUIT这行的意思是docker stop的时候发SIGQUIT而不是默认的SIGTERMNginx 就会走优雅关闭流程。配合docker stop -t 30给的超时时间绝大多数长连接请求都能正常收尾。这个改动看起来只有一行但它决定了一次发布是平滑还是抖一下我把它列为自建镜像的必备项。提示CMD里那句daemon off;是容器能跑起来的前提。Nginx 默认以守护进程方式运行启动后自己 fork 到后台、前台进程退出容器一看主进程没了就直接停止。加了daemon off;它才留在前台。2.4 配置文件与静态资源的 COPY 顺序COPY指令看着最简单其实顺序设计直接影响日常构建效率。基本原则是按变化频率从低到高排列。COPY nginx.conf /etc/nginx/nginx.conf COPY conf.d/ /etc/nginx/conf.d/ COPY html/ /usr/share/nginx/html/nginx.conf是主配置一年改不了几次放最前面conf.d/下的站点配置可能一周改一次放中间html/是静态资源前端一发包就变放最后。这样前端更新的时候前两层的缓存全部命中只有最后一层重新构建几秒钟搞定。反过来如果先 COPYhtml/那每次前端发包后面的配置层全部缓存失效构建时间白白增加。还有一个细节COPY conf.d/ /etc/nginx/conf.d/结尾的斜杠表示拷贝目录内容而不是目录本身。这个语法细节很容易搞错两者差一层目录表现就是配置找不到、站点 404。我的习惯是本地目录结构和容器内目录结构保持一致用 Mind Map 式的映射关系去记本地conf.d/default.conf对应容器/etc/nginx/conf.d/default.conf一一对应不容易错。至于默认站点官方源装完之后/usr/share/nginx/html里有个欢迎页。我一般会用自己的index.html覆盖掉内容哪怕只是一个服务名加版本号也比留着 Nginx 的默认欢迎页强——默认页出现在生产环境说明部署流程里有个环节没走通是个明显的信号。3. 从零到可运行镜像的完整实操前面讲的是为什么这么设计这一章是具体怎么敲。我会把目录结构、完整文件内容、构建命令、验证步骤全都给出来你照着做一遍就能得到一个可用的镜像。所有路径、版本号都写成了可直接复制的形式唯一需要你替换的是镜像仓库地址。3.1 目录结构与 .dockerignore先在本地建一个干净的构建目录结构如下nginx-ubuntu/ ├── Dockerfile ├── .dockerignore ├── nginx.conf ├── conf.d/ │ └── default.conf └── html/ └── index.html.dockerignore这个文件很多人不写觉得构建上下文就几个小文件无所谓。但只要你的构建目录里混进了.git、node_modules、日志文件、或者前端构建产物构建上下文可能一下子涨到几百兆每次构建都要把这些东西打包发给 Docker daemon速度慢得离谱。所以这个文件必须写.git .gitignore node_modules *.log .DS_Store README.md注意别把html/和conf.d/排除掉了这两个是构建要用的。这个文件的作用范围和.gitignore类似但是给 Docker 用的两者不能混用——我见过把.gitignore的内容直接抄进.dockerignore结果把配置文件也排除掉的情况构建出来的镜像里配置是空的启动就报错。3.2 完整 Dockerfile 与 nginx.conf把前面所有设计拼起来完整的 Dockerfile 长这样FROM ubuntu:22.04 LABEL org.opencontainers.image.descriptionUbuntu 22.04 based Nginx image \ org.opencontainers.image.vendorinternal ARG NGINX_VERSION1.24.0-1~jammy ARG TZAsia/Shanghai ENV DEBIAN_FRONTENDnoninteractive \ TZ${TZ} RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ ca-certificates curl gnupg tzdata; \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \ echo $TZ /etc/timezone; \ curl -fsSL https://nginx.org/keys/nginx_signing.key \ | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg; \ echo deb [signed-by/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu jammy nginx \ /etc/apt/sources.list.d/nginx.list; \ apt-get update; \ apt-get install -y --no-install-recommends nginx${NGINX_VERSION}; \ apt-get purge -y --auto-remove gnupg; \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN set -eux; \ ln -sf /dev/stdout /var/log/nginx/access.log; \ ln -sf /dev/stderr /var/log/nginx/error.log; \ mkdir -p /var/cache/nginx COPY nginx.conf /etc/nginx/nginx.conf COPY conf.d/ /etc/nginx/conf.d/ COPY html/ /usr/share/nginx/html/ EXPOSE 80 443 STOPSIGNAL SIGQUIT HEALTHCHECK --interval30s --timeout3s --start-period10s --retries3 \ CMD bash -c /dev/tcp/127.0.0.1/80 exit 0 || exit 1 CMD [nginx, -g, daemon off;]对应的nginx.conf我在官方默认基础上做了几处针对性调整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 rt$request_time urt$upstream_response_time; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; keepalive_timeout 65; server_tokens off; client_max_body_size 20m; client_body_timeout 30s; gzip on; gzip_min_length 1024; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svgxml; include /etc/nginx/conf.d/*.conf; }server_tokens off;是我每台机器都会加的它把错误页和响应头里的 Nginx 版本号隐藏掉成本为零收益是少暴露一点信息。client_max_body_size 20m是对着业务实际的上传上限定的1m 的默认值在现代 Web 里基本不够用413 报错十有八九是它。log_format里加了rt和urt两个耗时字段排查慢请求的时候有没有这两个字段的差别是能定位和只能猜的差别。conf.d/default.conf给一个最小可用的静态站点server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ 404; } location /healthz { access_log off; return 200 ok\n; } }健康检查这里用了一个独立路径/healthz直接返回 200 不走磁盘比检查首页更快也更稳还顺手关掉了这个路径的访问日志避免健康检查把日志刷满。3.3 构建命令、打标签与体积核对构建命令本身不复杂但几个参数值得说docker build \ --build-arg NGINX_VERSION1.24.0-1~jammy \ --build-arg TZAsia/Shanghai \ -t registry.internal/library/nginx-ubuntu:1.24.0 \ -t registry.internal/library/nginx-ubuntu:latest \ .--build-arg让版本号可以从外部注入这样同一个 Dockerfile 能构建出不同版本的镜像CI 里传参就行。-t打两个标签是常规做法一个带具体版本的不可变标签给生产环境用一个latest方便本地随手跑。生产环境永远只用带版本的标签这是铁律latest的语义是最近一次构建不可追溯出了问题你连回滚到哪个版本都不知道。还可以加上--progressplain看详细构建日志尤其是排查 apt 卡住、网络慢这类问题时很有用。构建完成后核对体积和内容docker images registry.internal/library/nginx-ubuntu docker run --rm --entrypoint bash registry.internal/library/nginx-ubuntu:1.24.0 \ -c du -sh /var/lib/apt/lists /var/cache/apt 2/dev/null; nginx -v; nginx -V 21 | tr \n | grep module第一句看体积是不是在预期范围100–150MB 之间如果超过 200MB说明清理那一步没生效回去检查是不是把删除动作写到别的 RUN 里了。第二句同时做了三件事确认 apt 缓存是空的、确认 Nginx 版本号、确认编译进了哪些模块。nginx -V输出的模块列表特别有用比如你需要http_ssl_module、http_v2_module、http_realip_module在这份输出里直接能看到有没有不用猜。如果想逐层看体积分布可以用docker historydocker history --no-trunc --format {{.Size}}\t{{.CreatedBy}} \ registry.internal/library/nginx-ubuntu:1.24.0 | head -20这个命令能把每一层的大小和对应指令列出来哪一层异常大一眼就能看出来。我第一次做这个镜像的时候就是靠它发现apt-get purge gnupg单独一行反而增大了体积改成同层清理后省了差不多十兆。3.4 启动验证curl、nginx -V、健康检查镜像构建出来了不等于能用下面这套验证流程我每次都会走一遍五步之内能确认镜像是否健康。第一步前台试跑确认能启动docker run --rm -p 8080:80 registry.internal/library/nginx-ubuntu:1.24.0终端上应该看到 Nginx 的启动日志并且容器不退出。如果容器打印完nginx: configuration file /etc/nginx/nginx.conf test is successful就立刻退出了多半是daemon off;没写或者被CMD覆盖了。第二步另开终端发请求curl -I http://127.0.0.1:8080/ curl -s http://127.0.0.1:8080/healthz应该看到HTTP/1.1 200 OK和响应头里没有Server: nginx/1.24.0这样的版本信息只有Server: nginx。如果返回 403检查静态目录权限和index.html是否存在如果返回 404检查root路径和try_files。第三步确认日志能进docker logsdocker run -d --name nginx-demo -p 8080:80 registry.internal/library/nginx-ubuntu:1.24.0 curl -s http://127.0.0.1:8080/ /dev/null docker logs nginx-demo能看到访问日志说明软链生效了。这一步没做的话后面上线了排查问题会非常难受因为日志采集侧什么都收不到。第四步验证优雅退出docker stop -t 30 nginx-demo正常情况下它会在几秒内退出并且docker inspect看到的退出码是 0。如果是 137说明被SIGKILL强杀了检查STOPSIGNAL有没有生效。第五步确认健康检查状态docker inspect --format {{.State.Health.Status}} nginx-demo跑起来之后等几十秒状态应该从starting变成healthy。如果一直是unhealthy先用docker inspect --format {{json .State.Health}}看最后一次检查的输出。start-period设成 10 秒是因为 Nginx 启动很快但如果你的配置里有大量 upstream 健康检查或者 DNS 解析启动时间会拉长这时候要适当调大。4. 镜像瘦身、安全加固与非 root 运行能跑的镜像和能上生产的镜像之间隔着体积和安全两道关。这两块的优化手段有些是通用的有些是 Nginx 特有的分开讲。4.1 体积从哪来逐层核算与瘦身手段先把体积账算清楚才知道该从哪里下手。以ubuntu:22.04为底构建完成后各部分的典型量级如下层内容典型大小说明基础层 ubuntu:22.04约 78 MB无法进一步压缩除非换底座工具与 Nginx 安装层约 45–55 MB含 curl、tzdata、Nginx 及依赖清理后的残留几 MB主要是配置和软链配置与静态资源小于 1 MB取决于你的前端产物合计约 130–145 MB与官方 Debian 版同量级第一刀砍在 apt 缓存上/var/lib/apt/lists在apt-get update之后能占到 20–30MB必须在同层删掉。第二刀砍在 gnupg 上拉完公钥就没用了同层 purge 掉能省几兆。第三刀是curl如果健康检查用/dev/tcp就不需要它但保留着调试方便几兆的代价我认为值得。第四刀是文档和语言包可以加--no-install-recommends之外再在 apt 里配APT::Install-Recommends 0;全局关掉推荐包效果更彻底。如果还想再瘦可以上 BuildKit 的缓存挂载把 apt 缓存单独挂出去不写进镜像层# syntaxdocker/dockerfile:1.4 RUN --mounttypecache,target/var/cache/apt,sharinglocked \ --mounttypecache,target/var/lib/apt,sharinglocked \ apt-get update apt-get install -y --no-install-recommends nginx这种写法的好处是多次构建之间能复用 apt 缓存构建速度明显提升同时缓存不进入镜像层体积干净。注意第一行必须是# syntaxdocker/dockerfile:1.4否则语法不被识别。启用 BuildKit 的方式是构建时加DOCKER_BUILDKIT1或者用新版 Docker 默认开启。最后提一句多阶段构建。如果你的需求最终变成了从源码编译 Nginx 并带上某个第三方模块那就该上多阶段了。第一阶段用完整的构建工具链gcc、make、libpcre3-dev、zlib1g-dev、libssl-dev编译出二进制和模块第二阶段从第一阶段只COPY --frombuilder二进制文件运行库靠 apt 装最低限度的版本。这样能把编译器那几百兆彻底甩掉。但要清楚多阶段解决的是编译期依赖问题如果你只是 apt 装包多阶段没有任何收益别为了显得专业硬上。4.2 以非 root 用户运行的正确姿势默认情况下容器里的 Nginx master 进程是以 root 启动的然后按user nginx;指令把 worker 进程降权到 nginx 用户。这在很多安全基线里是不合格的要求 master 也以普通用户运行。改造起来有几个必须同步调整的地方少改一个就起不来。第一个是端口。Linux 里监听 1024 以下的端口需要特权非 root 用户监听 80 会直接报Permission denied。解法是把listen 80;改成listen 8080;然后容器启动时用-p 80:8080做端口映射。对外还是 80容器内部是 8080这个转换用户完全感知不到。第二个是 pid 文件和缓存目录。/var/run/nginx.pid默认需要 root 才能写非 root 启动会报nginx: [emerg] open() /var/run/nginx.pid failed (13: Permission denied)。解法是把 pid 路径改到可写位置比如/tmp/nginx.pid同时把/var/cache/nginx的临时目录也改到/tmp下pid /tmp/nginx.pid; http { client_body_temp_path /tmp/client_body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; uwsgi_temp_path /tmp/uwsgi; scgi_temp_path /tmp/scgi; }第三个是user指令。非 root 启动 master 的时候user nginx;这行会失效并打出一条警告the user directive makes sense only if the master process runs with super-user privileges, ignored。这条警告无害但会污染日志可以按条件删掉或者直接忽略。对应的 Dockerfile 改造RUN useradd --system --uid 10001 --shell /usr/sbin/nologin nginxapp \ chown -R nginxapp:nginxapp /usr/share/nginx/html /etc/nginx/conf.d USER 10001 EXPOSE 8080注意USER之后的所有指令包括CMD都以该用户身份执行。还有一点容易忘日志软链指向/dev/stdout这个在非 root 下依然可用因为标准输出本来就属于该进程。但如果你改成写真实文件就得确保目录可写。4.3 安全扫描与最小化原则镜像做完之后建议过一遍扫描看有没有高危漏洞。常用的方式是在 CI 里挂一个扫描步骤对构建出来的镜像做一次检查把结果作为流水线的门禁——高危漏洞直接阻断发布。这个动作看起来麻烦但比出事之后回溯要省事得多。最小化原则体现在几个方面。一是不要装无关的包vim、net-tools、iputils-ping这些调试工具在开发阶段很方便进生产镜像就是多余的攻击面需要调试的时候用docker exec临时装或者用专门的调试镜像。二是不要在镜像里放敏感信息任何密码、密钥、证书私钥都不许进 Dockerfile通过环境变量或者挂载注入。三是固定基础镜像版本并定期重建基础镜像的补丁更新不会自动传导到你的镜像里必须主动重新构建才能拿到。还有一个实操建议给镜像打上标准标签包括版本、构建时间、源码提交号用LABEL或--label注入。线上出问题的时候docker inspect一眼就能看出这个镜像是哪个版本、什么时候构建的这个信息在应急时非常值钱。5. 常见问题与排查实录下面这些坑绝大多数我都亲身踩过至少一次。按现象、原因、解法整理成速查表遇到问题先对照着看能省不少时间。5.1 启动即退出与端口冲突容器刚启动就退出docker ps看不到。这是最高频的问题九成是主进程没留在前台。检查CMD里有没有-g daemon off;注意是分号结尾并且带引号写成daemon off不加分号 Nginx 会报配置语法错误。另一个可能是CMD被docker run后面跟的命令覆盖了比如你写了docker run image bash那容器跑的就是 bash跑完就退出。报nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)。容器内的 80 被占了常见原因是配置文件里有两个server块都写了listen 80;且没有server_name区分或者conf.d/下有一份默认配置和你自己的配置重复了。排查方式是在容器里跑nginx -T把所有生效配置打印出来看listen 80出现了几次。注意nginx -T是展开 include 之后的完整配置比-t只做语法检查更适合排查冲突。docker run -p 8080:80报端口已被占用。宿主机上的 8080 被别的进程占了。Linux 下用ss -lntp | grep 8080找出来换端口或者停掉冲突进程。5.2 日志、权限与 413 的典型坑docker logs是空的。检查两条软链有没有建成功docker exec 容器 ls -l /var/log/nginx/应该看到access.log - /dev/stdout。另一个可能是access_log off;覆盖了全局配置或者请求根本没达到 Nginx比如被上游代理拦了。报Permission denied打不开 pid 或缓存目录。如果你改成了非 root 运行检查pid路径和所有*_temp_path是否指向可写目录。默认的/var/run和/var/cache/nginx对非 root 用户不可写一定要改。上传文件返回 413。直接改client_max_body_size比如20m。这个值的单位是兆不要写成20M虽然大多数情况下大小写不敏感但配置项里统一用小写更保险。改完记得nginx -s reload或者重启容器配置不会自动生效。时间显示不对日志时间戳差八小时。检查TZ环境变量和/etc/localtime软链。有一种情况是tzdata装了但没建软链另一种是容器继承了宿主机的时区但配置文件里写死了。最稳的方式是在 Dockerfile 里就设好不要依赖运行时注入。访问日志里的中文变成问号。Ubuntu 基础镜像默认 locale 是C装一个locales包并生成en_US.UTF-8能解决大部分乱码但如果你的业务真的需要处理中文路径建议还是统一用英文命名从源头上避免编码问题。5.3 排查速查表把上面这些整理成一张表出问题的时候按现象查现象最可能的原因处理方式容器启动即退出daemon off缺失或 CMD 被覆盖检查 CMD用docker run -it --entrypoint bash进去手动起403 Forbidden静态目录权限不足或缺少 index 文件检查ls -l /usr/share/nginx/html和目录可执行位404 Not Foundroot路径错误或try_files配置不当用nginx -T看展开后的配置413 Request Entity Too Largeclient_max_body_size太小调到业务实际上限如20mopen() pid failed非 root 用户无写权限pid 改到/tmp并同步改所有 temp_pathUnable to locate package nginx源代号写错或 apt 索引过期核对 jammy/nobleupdate 与 install 同层构建卡住不动装了交互式包未设DEBIAN_FRONTEND加ENV DEBIAN_FRONTENDnoninteractive镜像比预期大 30MB清理动作跨层了把删除和安装合并到同一 RUNdocker logs无输出日志软链未建或被 off检查/var/log/nginx下软链健康检查一直 unhealthystart-period太短或检查路径返回非 200调大启动宽限检查/healthz配置表格之外再补两个不太常见但很折磨人的。一是hash sum mismatch通常发生在使用中间缓存或者网络环境不稳定的情况下解法是--no-cache重新构建如果还不行就在apt-get update前加一句rm -rf /var/lib/apt/lists/*。二是构建时公钥拉取失败表现为gpg: no valid OpenPGP data found原因多半是 curl 拿到了错误页面加-f参数让它明确失败然后检查网络是否可达。还有一个我自己的习惯每次镜像构建完成后先跑一个冒烟测试脚本用docker run -d起容器、curl打健康检查、拿到 200 再docker rm -f。这几十秒的验证能挡掉绝大多数镜像构建成功了但跑不起来的问题比推到仓库再被下游发现要体面得多。我在实际维护这套东西的过程中体会最深的一点是镜像这东西的价值不在于它第一次能不能跑起来而在于半年后你要改一行配置时它是让你花五分钟还是花五小时。把版本锁死、把清理写进同一层、把日志接到标准输出、把退出信号设对这些动作每一个都只花几十秒但它们决定了这个镜像是能用还是好维护。我现在给新同事的第一条建议也是这个先别急着优化体积把上面这四件事做对剩下的再说。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑