多阶段构建是通过分离构建与运行环境,仅将编译产物复制到精简运行镜像中,从而显著减小体积、提升安全性。它明确划分builder(装工具链、编译)和runner(仅含运行时依赖)两阶段,基础镜像可不同,依赖复制需精准控制粒度,避免冗余文件进入终态镜像。

多阶段构建不是简单拆成两个 FROM,而是用“构建归构建、运行归运行”的逻辑,把编译依赖、临时文件、调试符号这些只在构建期需要的东西,彻底挡在最终镜像之外。
明确划分 builder 和 runner 两个阶段
builder 阶段负责装工具链、编译 C 扩展、生成字节码;runner 阶段只保留运行必需的 Python 解释器、已编译好的库和业务代码。两者基础镜像可以不同——builder 用带 build-essential 的 slim,runner 用更精简的 slim 或甚至 scratch(若确认无动态链接依赖)。
- builder 镜像中安装 gcc、libffi-dev、openssl-dev 等仅用于编译的系统包,安装完立刻用 apt-get clean && rm -rf /var/lib/apt/lists/* 清理包索引
- runner 镜像不装任何编译工具,也不运行 pip install,所有依赖都靠 COPY --from=builder 复制过去
- 避免在 runner 阶段执行 pip install,否则会把 wheel 缓存、.pyc、__pycache__ 全部固化进镜像层
精准控制依赖的复制粒度
不要 COPY 整个虚拟环境或 site-packages 目录。那里面混着 .pyc、测试模块、文档、源码压缩包甚至 pip 解压后没删干净的临时文件。
- 在 builder 中用 pip install --no-cache-dir --target /app/deps -r requirements.txt,把依赖装到独立目录
- runner 阶段只 COPY --from=builder /app/deps /usr/local/lib/python3.11/site-packages/,跳过无关内容
- 若用用户级安装(--user),则 COPY --from=builder /root/.local /root/.local,并设好 PYTHONPATH 或 PATH
- 对含 C 扩展的包(如 numpy、psycopg2),优先查有没有对应平台的 wheel;没有就接受源码编译,但务必确保 builder 阶段清理了 /tmp/pip-build-*
规避 Alpine 的 musl 兼容陷阱
Alpine 看似轻量,但 musl libc 导致大量 wheel 无法复用,强制源码编译,反而增大体积、延长构建时间、引入不确定性。
- 除非项目明确要求 Alpine(如嵌入式或合规限制),否则首选 python:3.11-slim:体积约 110MB,glibc 兼容性好,绝大多数 wheel 可直接安装
- 若必须用 Alpine,改用 apk add 安装系统预编译包(如 py3-numpy),并加 && apk del .build-deps 清理构建依赖
- 禁用 pip 缓存:--no-cache-dir,否则 /root/.cache/pip 会成为隐藏体积大户
- 安装后手动删掉 .so 文件里的调试符号:strip --strip-unneeded *.so(需在 builder 阶段做)
利用 Docker 层缓存提升构建效率
体积瘦身和构建提速是一体两面。Docker 按指令逐层缓存,把不易变的内容放在前面,频繁变更的代码放在后面。
- 先 COPY requirements.txt,再 RUN pip install —— 这样只要依赖没变,后续构建就直接复用这一层
- 业务代码用 COPY app.py /app/app.py 和 COPY src/ /app/src/ 替代 COPY . /app,避免把 .git、tests、docs 带进去
- 在 runner 阶段设 ENV PYTHONDONTWRITEBYTECODE 1,防止容器启动时自动生成 __pycache__
- 终态镜像里不保留 .py 源码也可行:只放 .pyc + .so,但需确保 import 路径和 __pycache__ 目录结构正确
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











