03 · 镜像构建进阶:分层、多阶段、体积优化
D01 讲了"怎么写出能跑的 Dockerfile",本篇讲"怎么把镜像构建快、体积小、更安全"。三层能力:看图层、控体积、防泄漏。先给结论:镜像越小,拉取越快、攻击面越小、构建缓存越容易被复用。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 镜像体积、构建缓存与安全性的进阶三板斧 |
| 核心内容 | 图层心智、.dockerignore、多阶段构建、体积优化、镜像安全 |
| 读完能回答 | 为什么镜像要分层?多阶段构建解决了什么?怎么把 1GB 压到 200MB? |
| 相关板块 | D01 · Docker | D02 · 部署 CI/CD | C17 · 部署 |
一、先给结论:三个数字的优化
text
构建时间:依赖层命中缓存 → 秒级跳过
镜像体积:只留运行所需 → 几百 MB 变几十 MB
安全面:不带源码/密钥/调试器 → 漏洞更少一句话:Dockerfile 的顺序、上下文和阶段划分,决定了这三项。先从"慢"和"大"开始优化,安全随之而来。
二、图层心智:为什么顺序决定缓存
2.1 每一层都是文件系统快照
RUN / COPY 每执行一条就生成一层。构建缓存按层命中:前面没变,后面直接复用。
dockerfile
# ❌ 慢:代码在最前,每次改动都重装依赖
COPY . .
RUN pip install -r requirements.txt
# ✅ 快:依赖锁定在前,代码在后
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .2.2 缓存失效的精确度
| 写法 | 缓存行为 |
|---|---|
COPY . . | 任意文件变化 → 该层起全部重建 |
COPY requirements.txt . | 只有依赖文件变化才重建依赖层 |
RUN apt-get update 无锁 | 永远重新跑,且可能拿到不同结果 |
前端类比:像
npm install与node_modules缓存——只要package-lock.json没变,就不该重装。
三、.dockerignore:先缩小构建上下文
dockerfile
# .dockerignore
.git
node_modules
dist
*.md
.env| 作用 | 说明 |
|---|---|
| 缩小上下文 | COPY . . 只打包必要文件,构建更快 |
| 防泄漏 | .env、密钥、历史数据不进镜像 |
| 防污染 | 排除本机 node_modules/__pycache__ 覆盖容器内文件 |
铁律:
.gitignore拦不住的密钥,用.dockerignore再拦一次。镜像会被拉取、被扫描,任何进过镜像的敏感信息都算泄露。
四、多阶段构建:编译产物与运行环境分离
dockerfile
# 阶段 1:构建(带全量工具链)
FROM node:20 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2:运行(只留产物)
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "server.js"]| 好处 | 说明 |
|---|---|
| 体积小 | 编译器、测试工具不进入最终镜像 |
| 更安全 | 构建工具链的漏洞不带到线上 |
| 更清晰 | 构建与运行依赖彻底分离 |
Python 对照:构建阶段装
build-essential编译扩展,运行阶段只留编译好的.so与--omit=dev依赖。
五、体积优化清单
| 手段 | 效果 | 注意 |
|---|---|---|
| 用 slim/distroless 基础镜像 | 砍掉 shell/包管理器 | distroless 不方便进容器调试 |
| 多阶段只拷贝产物 | 避免工具链残留 | 保证 COPY --from 路径正确 |
| 清理安装缓存 | apt-get clean、rm -rf ~/.cache | 要在同一 RUN 里做 |
| 依赖去 dev | npm ci --omit=dev / pip --no-dev | 别把测试框架带进镜像 |
| 压缩产物 | 静态资源 gzip/br | 减少分层体积 |
判断顺序:先看每层多大(
docker history),最大层优先处理,别在 10MB 的层上抠。
六、镜像安全:不只是体积
| 风险 | 做法 |
|---|---|
| 以 root 运行 | 创建非 root 用户,USER app |
| 密钥进镜像 | 构建期注入(--secret),运行期环境变量 |
| 带调试器/源码 | 多阶段只拷运行产物 |
| 基础镜像漏洞 | 锁定 tag、定期 docker scan/SCA 扫描 |
| 无固定版本 | FROM python:3.12-slim 比 :latest 可复现 |
与 S07 的关系:镜像里的依赖也要走 S07 供应链扫描,
docker scan就是容器层的 SCA。
七、自检清单
- [ ] Dockerfile 把"不变的依赖"放前、"易变的代码"放后
- [ ] 有
.dockerignore,且确认不含密钥/.git - [ ] 用多阶段构建,运行镜像不含编译工具链
- [ ] 基础镜像锁版本,用 slim/distroless 且知道取舍
- [ ] 容器内非 root 运行,密钥不落镜像
- [ ] CI 里跑了镜像漏洞扫描
八、常见误区
| 误区 | 事实 |
|---|---|
| "代码放前面也没事" | 一行代码改动会连带重装依赖,构建慢几倍 |
| ".gitignore 就够了" | 镜像上下文是另一条泄露路径,要 .dockerignore 兜底 |
| "多阶段只是为了体积" | 体积小、攻击面小、依赖边界清晰是三重收益 |
| "slim 一定最好" | distroless 无 shell,调试和生产排障要换思路 |
| "镜像小就安全" | 运行用户、密钥、基础镜像漏洞同样决定安全性 |
九、动手练习
练习 A:看你的层
对自己项目跑 docker build,再用 docker history <镜像> 找出体积最大的三层,记录占比。
练习 B:压体积
把现有镜像改成多阶段 + .dockerignore,记录优化前后体积和构建时间,目标至少减半。
练习 C:非 root 化
给一个容器加上 USER 指令与最小权限,验证容器内进程不再是 root,并确认服务仍正常。
相关文档
本区
- 01 · Docker 与容器化 —— 镜像与容器心智、Compose(本篇前置)
- 02 · 部署与 CI/CD —— 镜像 tag 与发布链路
- D 域 README
其他域
- C17 · 部署与 CI/CD —— 后端容器化入门
- S07 · 供应链与依赖安全 —— 依赖与镜像扫描
- S06 · 运维安全 —— 密钥与最小权限