用 Docker 多阶段构建把 Python 应用镜像瘦下来
很多人第一次给 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 镜像,收成一个职责更清晰的运行镜像。体积会下降,构建日志更好读,后续换基础镜像或升级依赖时,也不用在同一个阶段里反复权衡编译工具该不该删。
- 点赞
- 收藏
- 关注作者
评论(0)