Docker 镜像从 1.2GB 压到 180MB:一个 Python 服务的分层瘦身实录

举报
yd_255409349 发表于 2026/11/01 05:39:33 2026/11/01
【摘要】 一个 FastAPI 服务初版镜像 1.2GB、冷启动 40 秒。本文记录了从量化体积构成、多阶段构建、基础镜像选型、缓存清理到层顺序优化的完整过程,最终压到 180MB,并说明哪些东西不该为了瘦身砍掉。

上周把一个 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` 和缓存目录压根没被排除。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。