多阶段构建:把 Python 服务镜像从 1GB 瘦下来
很多团队第一次把 Python 服务打进 Docker,镜像动辄七八百兆甚至超过 1GB。拉取慢、发布慢,漏洞扫描还总是红一片。体积大通常不是业务代码的错,而是基础镜像、构建工具和缓存层叠在了一起。下面按一条可复现的排查顺序,把镜像瘦下来。
先看现状。在构建机上执行 docker image ls,记下目标镜像的大小;再用 docker history 看每一层新增了多少。如果某一层突然增加几百兆,多半是 apt 或 pip 安装时把编译器和缓存留在了同一层。history 只能看到层大小,看不到具体文件。这时进入容器,用 du -h -d 1 / 就能定位是 /usr、/root/.cache 还是 /var/lib/apt。
第二步换基础镜像。开发阶段可以用 python:3.12,里面带 gcc 和头文件,方便安装带 C 扩展的包。发布阶段不要沿用这张镜像。python:3.12-slim 基于 Debian slim,一般能少掉一半以上。若依赖都很纯,可以试 python:3.12-alpine,体积更小,但 musl 会让部分带预编译轮子的包装不上,一报错就退回 slim,不要硬刚。
第三步用多阶段构建。第一阶段命名为 builder,安装编译依赖,并用 pip install --prefix=/install 把依赖装到独立目录。第二阶段只从 slim 开始,用 COPY --from=builder 把 /install 拷到 /usr/local,再复制业务代码。编译器、wheel 缓存和 apt 列表都留在 builder,不会进入最终镜像。注意不要在最终阶段再跑一次 pip install,否则前面的瘦身等于白做。
第四步管好层缓存和垃圾文件。把 requirements.txt 单独 COPY,然后再 COPY 源码,这样改一行业务代码不会让依赖层失效。同一条 RUN 里安装完立刻删除缓存:清理 apt 列表和 /root/.cache。拆成两条 RUN 再删除是没用的,因为前一层已经提交。另外准备一份 .dockerignore,排除 .git、虚拟环境、pycache、测试目录和本地 .env,避免把虚拟环境和密钥打进镜像。
第五步用非 root 运行。在最终阶段创建普通用户,把应用目录授权给它,再用 USER 切换。这不直接减小体积,但能减少扫描里的高危项,也避免容器被突破后直接拿到 root。
最后做验收。同一份代码分别用单阶段和多阶段构建,对比 docker image ls 的大小,并用同一条启动命令跑健康检查。瘦身前后接口行为应一致。一个 FastAPI 示例里,镜像从大约 1.05GB 降到大约 180MB,家庭带宽下拉取时间从两分钟降到二十秒左右。数字会随依赖变化,方法是稳定的:先定位大层,再把构建工具关在 builder 里,最终镜像只保留运行时需要的文件。
- 点赞
- 收藏
- 关注作者
评论(0)