用 Docker 多阶段构建把 Python 应用镜像瘦下来

举报
yd_240066378 发表于 2026/11/12 08:39:05 2026/11/12
【摘要】 很多人第一次给 Python 服务写 Dockerfile,会习惯性地把构建依赖和运行依赖塞进同一个镜像:先 apt-get 装 gcc 和 libffi-dev,再 pip install -r requirements.txt,最后把源码拷进去。镜像能跑,但体积动辄 1GB 以上,冷启动慢,还更容易把编译工具和缓存带进生产环境。多阶段构建要解决的,就是把“编译时需要的东西”和“运行时真正...

很多人第一次给 Python 服务写 Dockerfile,会习惯性地把构建依赖和运行依赖塞进同一个镜像:先 apt-get 装 gcc 和 libffi-dev,再 pip install -r requirements.txt,最后把源码拷进去。镜像能跑,但体积动辄 1GB 以上,冷启动慢,还更容易把编译工具和缓存带进生产环境。多阶段构建要解决的,就是把“编译时需要的东西”和“运行时真正需要的东西”拆开。

为什么镜像会臃肿

以一个常见的 Flask 或 FastAPI 服务为例。requirements.txt 里如果有 pydantic、cryptography、psycopg2 这类带原生扩展的包,pip 在构建时往往需要 gcc、make、Python 头文件。这些编译器和头文件只在安装阶段有用,容器真正跑起来时,只要有 Python 解释器、已编译好的 wheel,以及应用代码即可。如果全程只用一个 FROM python:3.12,构建工具会一直留在最终镜像里,apt 和 pip 的缓存也会占掉几百 MB。

另外一个容易忽略的点是构建上下文。如果项目根目录有 .git、测试数据、本地 venv 或 .env,而 Dockerfile 里又写了 COPY . .,这些文件会进镜像,既增大体积,也可能把本地密钥带出去。所以 .dockerignore 和多阶段构建要一起用。

一个最小可运行的写法

第一阶段专门负责安装依赖。可以用官方 slim 镜像,先安装编译依赖,再把依赖装到一个独立前缀,例如 /install。第二阶段只复制 Python 运行时和已经装好的包,不再执行 apt-get 和 pip。大致结构如下:

FROM python:3.12-slim AS builder
WORKDIR /build
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.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app ./app
USER 65534:65534
CMD ["python", "-m", "app"]

这里有三个值得坚持的细节。第一,pip 加上 --no-cache-dir,避免把下载缓存留在构建层。第二,最终阶段不要再 COPY requirements.txt 之后现场安装,而是只拷贝已经安装好的目录。第三,容器不要用 root 跑应用,能提前暴露权限和文件所有权问题。

构建时怎么验收

镜像写完不要只看“能启动”。至少做三件事:用 docker images 对比单阶段和多阶段的体积;用 docker history 看哪些层最胖;进到容器里确认没有 gcc,并且应用的健康检查接口能在几秒内返回。如果体积没有明显下降,多半是最终阶段又把 builder 的系统包拷进去了,或者构建上下文混进了无关文件。这时先检查 .dockerignore,再确认 COPY --from=builder 的路径是安装前缀,而不是整个根文件系统。

多阶段构建不是把 Dockerfile 写复杂,而是给镜像一个明确边界:构建环境可以脏、可以大,运行环境必须小、必须干净。把这条边界守住,Python 服务的镜像才会既好发,也更好维护。

.dockerignore 别忘了写

多阶段构建解决的是镜像内部的层,.dockerignore 解决的是构建上下文。建议至少忽略 .git、.venv、pycache、.env、测试产物和本地 IDE 配置。这样 docker build 不会把它们发给守护进程,COPY . . 即使写得稍微宽松一些,也不至于把密钥或虚拟环境打进去。一个最小文件可以是:

.git
.venv
__pycache__/
*.pyc
.env
.env.*
.idea
.vscode
tests
.pytest_cache

注意 .env.* 可能连 .env.example 一并排除,如果示例文件需要进镜像,就写得更精确一些,例如只忽略 .env 和 .env.local。原则很简单:默认不进镜像,真正需要的文件再显式 COPY。

按上面的拆分做完,一般能把一个“能跑但不适合分发”的 Python 镜像,收成一个职责更清晰的运行镜像。体积会下降,构建日志更好读,后续换基础镜像或升级依赖时,也不用在同一个阶段里反复权衡编译工具该不该删。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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