一个典型的 Python Docker 镜像,起步就是 900MB 甚至更大。大部分体积来自你根本不会用到的东西——系统包、pip 缓存、编译中间产物、测试文件。镜像越大,拉取越慢,部署越贵,安全扫描的噪音越多。这篇文章把常用的瘦身手法逐个拆开,给出可以直接复制改造的 Dockerfile 和命令。
先看看问题有多大
用最朴素的方式打包一个 Flask 项目:
FROM python:3.12
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
构建完 docker images 一看,轻松超过 1GB。其中 Python 官方完整镜像本身就占了约 900MB——它带着完整的 Debian 发行版、编译工具链、man 页面、locale 数据。你的应用只需要其中一小部分。
第一步:换掉基础镜像
最直接的改动:把 python:3.12 换成 python:3.12-slim。
FROM python:3.12-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
slim 变体删掉了 gcc、man 页面等大量非运行时必需的内容,镜像体积直接降到约 150MB 起步。绝大多数纯 Python 项目(不涉及 C 扩展编译)用 slim 就够了。
Alpine 的诱惑与陷阱:python:3.12-alpine 只有约 50MB 基础体积,看起来更极致。但 Alpine 用 musl libc 而非 glibc,许多 Python C 扩展(numpy、pandas、cryptography 等)需要现场编译,编译过程反而引入更大的依赖链,最终镜像可能比 slim 还大,构建时间也更长。如果你的依赖全是纯 Python 包,Alpine 可以考虑;否则,slim 是更稳妥的选择。
第二步:干掉 pip 缓存和中间产物
pip 默认把下载的 wheel 缓存到 /root/.cache,这些文件在镜像里毫无用处。一行参数就能清除:
RUN pip install --no-cache-dir -r requirements.txt
--no-cache-dir 让 pip 不写缓存目录,省下几十到上百 MB。
还可以在安装后删除编译残留和不需要的文件:
RUN pip install --no-cache-dir -r requirements.txt \
&& find /usr/local -type d -name __pycache__ -exec rm -rf {} + \
&& find /usr/local -type f -name '*.pyc' -delete \
&& find /usr/local -type f -name '*.pyo' -delete
第三步:多阶段构建,只带走成品
多阶段构建是瘦身最有效的一招:在一个"构建阶段"安装所有依赖甚至编译 C 扩展,然后在"运行阶段"只复制最终产物,构建工具链全部丢弃。
# ---- 构建阶段 ----
FROM python:3.12 AS builder
WORKDIR /install
COPY requirements.txt .
RUN pip install --prefix=/install/deps --no-cache-dir -r requirements.txt
# ---- 运行阶段 ----
FROM python:3.12-slim
COPY --from=builder /install/deps /usr/local
COPY . /app
WORKDIR /app
CMD ["python", "app.py"]
关键点:pip install --prefix=/install/deps 把包装到指定目录,运行阶段用 COPY --from=builder 只把安装好的包搬过来。gcc、make、头文件这些编译工具全部留在 builder 阶段,不会进入最终镜像。
如果你的项目有 C 扩展依赖(比如 cryptography),这种手法尤其有效——构建阶段用完整镜像编译,运行阶段用 slim 只带编译好的 so 文件。
第四步:用 .dockerignore 排除无关文件
Docker 构建上下文里混进 .git、__pycache__、.venv、测试数据,都会让 COPY 拖入无用内容。在项目根目录创建 .dockerignore:
.git
.gitignore
__pycache__
*.pyc
*.pyo
.venv
venv
.env
*.md
tests/
.pytest_cache
.mypy_cache
.dockerignore
Dockerfile
docker-compose.yml
这不会减小镜像层本身,但能避免 COPY 把本地垃圾带进去,也让构建上下文发送更快。
第五步:虚拟环境 + 精准复制
另一种常见做法:在 builder 中创建虚拟环境,运行阶段只复制整个 venv 目录。好处是路径干净、卸载方便。
# ---- 构建阶段 ----
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
# ---- 运行阶段 ----
FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
COPY . /app
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "app.py"]
运行阶段甚至可以不用任何 Python 基础镜像,直接用更小的 Debian 或 Ubuntu 最小镜像 + 手动复制 venv 里的 Python 解释器,但这需要更多调试,适合极端体积敏感场景。
一次性可运行的完整示例
下面是一个可以直接跑的完整项目结构,把上面所有手法串起来。假设你有一个最简 Flask 应用。
app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello from a slim container!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
requirements.txt
flask==3.0.3
Dockerfile
# 构建阶段:安装依赖
FROM python:3.12 AS builder
WORKDIR /install
COPY requirements.txt .
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
# 运行阶段:只带成品
FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
COPY . /app
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
EXPOSE 5000
CMD ["python", "app.py"]
.dockerignore
.git
__pycache__
*.pyc
.venv
tests
*.md
构建并查看体积:
docker build -t slim-flask .
docker images slim-flask
对比朴素版本:
# 朴素版本 Dockerfile(仅用于对比)
cat > Dockerfile.big <<'EOF'
FROM python:3.12
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.big -t big-flask .
docker images big-flask
在我的测试中,big-flask 约 1.05GB,slim-flask 约 130MB——瘦身 87%,而且功能完全一致。
选型决策清单
| 场景 | 推荐做法 | 预期体积 |
|---|---|---|
| 纯 Python 依赖,无 C 扩展 | slim + --no-cache-dir |
120-180MB |
| 有 C 扩展(numpy 等) | 多阶段构建,builder 用完整镜像,运行用 slim | 150-250MB |
| 极端体积要求,纯 Python | Alpine + --no-cache-dir |
50-80MB |
| 极端体积要求,有 C 扩展 | 多阶段构建,builder 用完整镜像,运行用 Alpine(需测试兼容性) | 80-120MB |
| 生产环境,安全优先 | slim + 多阶段 + .dockerignore + 定期安全扫描 | 120-200MB |
几个容易踩的坑:
- Alpine + C 扩展:构建时间暴增,最终体积可能反而更大。先用 slim 试,确认没问题再考虑 Alpine。
- 多阶段构建中路径问题:
--prefix安装和 venv 复制两种方式路径不同,运行阶段要确保PATH正确设置,否则import找不到包。 - 删 pycache 的时机:如果放在同一个 RUN 层里和 pip install 一起做,不会产生额外层;如果分成两个 RUN,中间层会保留被删除的文件,镜像体积反而更大。Docker 层合并是瘦身的隐性规则——尽量把 rm 操作和产生文件的 RUN 合并成一条。
把镜像做小不是一次性的事。每次加新依赖后跑一次 docker images 看看体积变化,比事后补救省得多。