Docker 镜像从 1.2GB 压到 180MB:一个 Python 服务的分层瘦身实录
上周把一个 FastAPI 服务从虚拟机迁到容器,第一版 Dockerfile 写完,`docker images` 一看 1.2GB,推镜像要六分钟,拉起来到能接流量 40 秒。花了一个下午做分层优化,最终 180MB、启动 3 秒。过程里的几个判断比"多阶段构建"这四个字更有价值,记一下。
## 先量化,别急着改
上来就换基础镜像很容易白忙。先用两条命令看清体积都去哪了:
```bash
docker history --no-trunc myapp:latest | awk '{print $NF, $5}'
docker run --rm -it --entrypoint sh myapp:latest -c 'du -sh /* 2>/dev/null | sort -h'
```
我这版的问题很典型:apt 源索引 120MB、pip 缓存 90MB、编译 numpy 带进来的 gcc 工具链 300MB、`.git` 目录 200MB。**工具链和 `.git` 占了将近一半**,这两样运行时完全用不到。
## 多阶段构建:只搬成品
```dockerfile
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends build-essential
COPY requirements.txt .
RUN pip wheel --no-cache-dir -r requirements.txt -w /wheels
FROM python:3.11-slim
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* \
&& rm -rf /wheels
```
要点是**在 builder 里编译,在 runtime 里只装 whl**。`pip wheel` 把编译产物固化成 wheel 文件,运行镜像里既不需要 gcc,也不需要 pip 去联网解析依赖。
## 基础镜像的选择
`python:3.11` 是 1GB 的完整 Debian,`python:3.11-slim` 只有 130MB 左右,差了一个数量级。很多人再进一步换 `alpine`,这里要小心:alpine 用 musl libc,numpy、pandas、psycopg2 这些带 C 扩展的包要么没有预编译 wheel、要么得从头编译,最后镜像反而更大,还多了 libc 兼容的坑。
我的判断标准很简单:**依赖里有编译型包就用 slim,纯 Python 且体积敏感才考虑 alpine。**
## 三条几乎零成本的清理
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
```
`/var/lib/apt/lists` 是 apt 的索引缓存,装完就没用了,这一条能省 100MB 以上。`--no-cache-dir` 让 pip 不把下载的包再存一份。`--no-install-recommends` 挡住 apt 顺手装的一堆推荐包。
## `.dockerignore` 比想象中重要
`docker build` 会把整个构建上下文发给 daemon。没写 `.dockerignore` 时,`.git`、`venv`、`node_modules`、测试数据全被打包上传,既慢又可能被 `COPY . .` 带进镜像。至少要挡住:
```
.git
.venv
__pycache__
*.pyc
tests/
.env
```
## 层缓存:把变化频率低的放前面
```dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
```
依赖清单没变时,改业务代码不会触发重装依赖。顺序反过来,每次改一行代码都要重新装一遍,构建时间从 8 秒变成 3 分钟。
## 最后补上运行时的最小权限
```dockerfile
RUN useradd -r -u 10001 appuser
USER appuser
```
配合编排层的 `readOnlyRootFilesystem: true` 和 `allowPrivilegeEscalation: false`,一个被攻破的容器能做的事情就被限制住了。这部分不省体积,但成本和收益的比值最高。
## 结果和一点保留
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 镜像体积 | 1.2GB | 180MB |
| 推送耗时 | 6 分钟 | 40 秒 |
| 冷启动 | 40 秒 | 3 秒 |
要说保留:**不是越瘦越好**。我留了一个 `debug` 阶段装了 curl、vim、py-spy,线上排查问题时 `--target debug` 构建即可。为了极限体积把这些砍掉,出事时只能干瞪眼,不划算。
真正起作用的是"先看清体积构成"这一步——很多人跳过了它,直接抄一份多阶段 Dockerfile,结果发现镜像还是八百兆,因为 `.git` 和缓存目录压根没被排除。
- 点赞
- 收藏
- 关注作者
评论(0)