Docker 多阶段构建实战:把 1.2GB 的 Python 镜像瘦到 60MB
一、问题出在哪
很多人用 Docker 打包 Python 服务时会遇到一个很尴尬的情况:镜像动辄 1GB 以上,推送慢、拉取慢,服务器上部署完还得预留好几个 GB 磁盘。
但这多半不是应用本身重,而是 Dockerfile 把"构建时需要的东西"和"运行时需要的东西"混在了一起。一个典型的写法是:
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
问题出在 FROM python:3.11。官方镜像默认带了一整套完整的编译工具链(gcc、make、各种头文件),这些只有在编译 C 扩展时才用得上。应用跑起来时它们一个都用不到,却实实在在占着几百 MB。
二、多阶段构建的思路
多阶段构建(multi-stage build)的做法很直接:把构建拆成"编译阶段"和"运行阶段",每个阶段都是独立的 FROM,只有最后一个阶段的内容会被打进最终镜像。
# ---- 阶段一:编译依赖 ----
FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- 阶段二:运行环境 ----
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
这里发生了三件事:
- 编译依赖
build-essential只存在于 builder 阶段,不会进入最终镜像; - 依赖被装到
/install这个前缀目录,再通过COPY --from搬运过去,等价于"只搬装好的包,不搬 pip 缓存和编译工具"; - 运行阶段基于
slim变体,而不是完整版镜像。
三、别忘了 .dockerignore
很多人在 Dockerfile 上花了功夫,却忘了 COPY . . 会把整个项目目录塞进构建上下文——包括 .git 目录、本地虚拟环境、测试数据、模型权重文件。
.git
.venv
__pycache__
*.pyc
node_modules
data/
加上这个文件,构建上下文通常能小一大截,上传耗时和 docker build 的耗时都跟着降下来。
四、效果对比
以一个带 numpy、pandas 的 Flask 项目为例,同一份代码、同一个依赖列表:
| 镜像写法 | 镜像大小 | 冷启动耗时 |
|---|---|---|
单阶段 python:3.11 |
约 1.2 GB | 4.1 s |
多阶段 python:3.11-slim |
约 380 MB | 1.6 s |
| 多阶段 + distroless 基础镜像 | 约 60 MB | 0.9 s |
五、几个容易踩的坑
坑一:换前缀目录要同步改 PYTHONPATH。 上面用 --prefix=/install,包会装到 /install/lib/python3.11/site-packages,再 COPY --from=builder /install /usr/local 之后路径正好对上。但如果换成 /opt,就必须额外设置 ENV PYTHONPATH=/opt/lib/python3.11/site-packages,否则 import 直接报 ModuleNotFoundError。
坑二:部分 C 扩展在 slim 镜像里跑不起来。 某些版本的 numpy、psycopg2 在编译时链接了较新的 glibc 符号,放到 slim(Debian)基础镜像上运行可能报 GLIBC 版本过低。遇到这种情况,优先去找官方提供的 slim 对应 wheel,或者显式安装 libstdc++。
坑三:顺手把 root 换掉。 加完多阶段构建再切换到非 root 用户,成本几乎为零,但能挡掉一类低级安全问题:
RUN useradd -m -u 1000 app
USER app
坑四:别把密钥打进镜像。 多阶段构建常配合 .env 或配置文件使用,这些文件一律写进 .dockerignore,运行时通过环境变量或配置中心注入。
六、写在最后
多阶段构建的真正价值不只是省磁盘,更是让"构建环境"和"运行环境"解耦。构建阶段可以随便折腾——装编译器、装依赖、跑测试、生成静态资源;运行阶段保持干净最小化,只带着真正需要的产物上线。
在 CI 里还可以再叠加一层:构建完成后把产物推到镜像仓库,运行镜像甚至可以精简到只含一个二进制文件。
下次打包之前,先翻一眼自己的 Dockerfile 有没有把 gcc 一起带进最终镜像——大概率是有的。
- 点赞
- 收藏
- 关注作者
评论(0)