LOADING
2447 words
12 minutes
Docker 工程化实践(五):Docker 镜像优化最佳实践

Docker 工程化实践系列 · 第 5 篇

引言:能跑起来,就够了吗

经过前四篇的学习,你已经能够写出一份使用多阶段构建、缓存友好的 Dockerfile。镜像能正常构建,容器能正常启动,API 能正常响应——看起来一切都没问题。

但”能运行”和”适合生产环境长期运行”之间还有不小的距离。你的镜像是否以 root 用户运行?容器挂了之后有没有健康检查帮你自动发现?数据库密码是不是写死在镜像里?基础镜像的版本是不是飘忽不定的 latest?这些问题在本地开发时可能不会暴露,但在生产环境里,它们会变成凌晨三点的告警电话。

本文从安全、体积、可观测性三个维度,介绍生产级 Docker 镜像需要满足的标准和优化方法。

一、镜像大小为什么值得关注

很多人觉得磁盘现在很便宜,镜像大一点无所谓。但在实际工程中,镜像大小会直接影响三个环节。

部署速度。 一个 500MB 的镜像在 50MB/s 的带宽下拉取需要 10 秒,一个 2GB 的镜像则需要 40 秒以上。在 Kubernetes 自动扩容场景下,几十个节点同时拉取镜像——镜像越大,扩容响应越慢。一个本来能靠扩容扛住的流量尖峰,可能因为镜像拉取太慢而把已有节点也拖垮。

CI/CD 时间。 每次 Pipeline 都要重新构建镜像、推送到 Registry、服务器再拉取——镜像越大,每一步花费的时间越长,整个部署链条的耗时就越久。

安全攻击面。 镜像越大意味着里面包含的软件包越多,而每个额外的软件包都可能存在已知漏洞。一个完整 Debian 环境可能包含 bash、curl、python、gcc 等几十个工具,它们每一个都有各自的版本历史和 CVE 记录。缩小镜像不仅是节省空间,也是在减少潜在的攻击入口。

二、分析镜像体积:先看清问题在哪

优化之前需要先定位问题。docker images 可以看到镜像的总大小,但不知道空间具体消耗在哪里。docker history <image> 可以按层查看每一层增加了多少内容——通常你会发现最大的层来自依赖安装(node_modules)和基础镜像本身。docker system df 则可以查看 Docker 在磁盘上的总体占用情况(镜像、容器、数据卷各占多少),适合排查”为什么 Docker 磁盘占用越来越大”的问题。

拿到数据之后,优化就有了方向:如果基础镜像太大就换 slim 或 alpine;如果依赖安装的层太大就检查是否有不必要的依赖被打进去了;如果有构建缓存残留在镜像中就把清理步骤和安装步骤合并到同一个 RUN。

三、运行时安全加固

1. 不要用 root 用户运行容器

默认情况下,很多 Docker 容器内的进程以 root 身份运行。随便进入一个容器执行 whoami,你可能看到的就是 root。这意味着如果应用存在漏洞(比如任意代码执行),攻击者就能以 root 权限在容器内执行命令——虽然容器本身提供了一定隔离,但一旦配合内核提权漏洞,后果会非常严重。

解决方式是在 Dockerfile 中显式指定非 root 用户:

FROM node:22-slim
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "dist/server.js"]

node:22-slim 镜像中已经预置了 node 用户,直接用 USER node 切换即可。如果使用的基础镜像没有预置用户,可以用 RUN addgroup --system app && adduser --system --ingroup app app 自行创建。

2. 不要把密钥写进镜像

这是生产中非常严重的错误:

# ❌ 绝对不要这样做
ENV DATABASE_PASSWORD=123456
ENV API_SECRET=sk-xxxxxx

这些信息一旦写入镜像,就会永久保留在镜像历史中——任何能访问镜像仓库的人、能查看镜像层信息的人,都能看到你的密钥。即使后来删除了镜像,只要历史版本还在 Registry 中,密钥仍然存在。

正确的方式是运行时注入:

Terminal window
docker run -e DATABASE_PASSWORD=secret fastify-api
# 或者
docker run --env-file .env fastify-api

生产环境中,环境变量通常由部署系统(Kubernetes Secrets、GitHub Actions Secrets、Vault 等)在容器启动时注入,永远不会出现在镜像或 Dockerfile 中。

3. 固定版本,不要依赖 latest

# ❌ 不确定的版本
FROM node:latest
# ✅ 明确的版本
FROM node:22-slim
# 甚至是精确到小版本
FROM node:22.15-slim

latest 标签会随着时间变化——今天指向 Node.js 22,半年后可能变成 Node.js 24。这会导致同一个 Dockerfile 在不同时间构建出来的镜像不一致——很难排查,也很难复现。生产环境中,所有依赖的版本都应该显式声明、受控升级。

4. 镜像安全扫描

构建完成后,可以使用 docker scout cves <image> 扫描镜像中已知的安全漏洞。在企业 CI 流程中,安全扫描通常作为一个门禁——如果发现高危 CVE,构建会被拦截,镜像不会被推送到 Registry。这不是”过度工程”,而是生产环境的标配。

四、可观测性设计

1. HEALTHCHECK 健康检查

容器状态是”运行中”,不代表应用状态是”正常”。Node 进程可能还活着,但数据库连接已经断开,所有 API 请求都在返回 500 错误。Docker 本身不会知道这一点——除非你配置了 HEALTHCHECK:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl --fail http://localhost:3000/health || exit 1

Docker 会周期性地执行这个命令,根据返回值判断容器是否健康。配合 Docker Compose 或 Kubernetes,不健康的容器可以被自动重启或替换。对应的,你的应用需要提供一个 /health 端点用于检查——比如验证数据库连接、Redis 连接是否正常。

2. 日志输出到标准输出

很多初学者习惯把日志写到文件:

/app/logs/app.log

但在容器环境中,推荐的做法是把日志输出到 stdoutstderr。Node.js 中就是原生的 console.logconsole.error。这样做的好处是 Docker 自动收集这些输出(docker logs 可以查看),而且日志可以被外部日志系统(如 Fluent Bit → Loki → Grafana)统一采集和处理。

容器的职责是运行应用并输出日志到标准输出,日志的存储、轮转、聚合和分析应该交给外部的日志系统——这就是”关注点分离”在容器化环境中的体现。

五、生产级 Dockerfile 完整示例

综合前四篇和本篇的所有优化原则,一份合格的生产级 Node.js Dockerfile 如下:

# =====================
# Build Stage
# =====================
FROM node:22 AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# =====================
# Production Stage
# =====================
FROM node:22-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable \
&& pnpm install --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl --fail http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

这份 Dockerfile 包含了:多阶段构建(构建与运行分离)、生产依赖分离(--prod)、slim 基础镜像、非 root 用户运行、明确的端口声明和健康检查。一份典型的 TypeScript + Fastify 项目从单阶段 node:22 的 1.21.5GB 优化后,通常在 200400MB。

六、镜像优化原则总结

一个优秀的生产镜像应该满足六个原则。

最小化。 只包含运行应用所必需的内容——运行时、生产依赖、编译产物。源码、构建工具、测试框架、开发依赖都不应该出现在生产镜像中。

不可变。 镜像构建完成后不应手动修改。任何变更都应该通过修改 Dockerfile 后重新构建来实现,保证每次部署的镜像都来自同一个构建过程。

明确版本。 避免使用 latest 标签。基础镜像的版本、依赖的版本都应该明确声明,确保构建的可重复性。

最小权限。 不以 root 用户运行容器。为每个服务创建专用用户,只赋予必要的最小权限。

配置外置。 不把密钥、数据库密码等敏感信息写入镜像。所有环境差异(开发/测试/生产)的配置都在运行时注入。

可观测。 支持健康检查、输出日志到标准输出、暴露必要的监控指标。让容器不仅仅能运行,而且能让人知道它运行得怎么样。

七、总结

一个能运行的 Docker 镜像和一个适合生产环境长期运行的 Docker 镜像,之间的差距不在 Docker 命令本身,而在对安全、可维护性和工程规范的理解。小的镜像分发更快、攻击面更小;非 root 运行降低了安全风险;健康检查让故障可以自动发现;配置外置让镜像可以在不同环境间通用。

到这一篇为止,我们已经掌握了对”单个容器”进行从构建到优化的完整流程。但真实的后端项目几乎不可能只有一个容器——API、数据库、缓存、反向代理需要协同工作。下一篇将介绍 Docker Compose,用一个配置文件管理整个多容器应用。

上一篇:Docker 工程化实践(四):Docker 多阶段构建与生产镜像优化

下一篇:Docker 工程化实践(六):Docker Compose 管理多容器应用

Docker 工程化实践(五):Docker 镜像优化最佳实践
/posts/2026-7-15/5-docker-工程化实践五docker-镜像优化最佳实践/
Author
Atopos
Published at
2026-07-15
License
CC BY-NC-SA 4.0

Some information may be outdated