如何减小 Python 的 Docker 镜像的大小

标签:Python

虽然我对公司的屎山代码已经见怪不怪了,但是看到一个普通的 Python web 应用的 Docker 镜像大小超过 10 GB,还是让我感叹前人的智慧。
这些巨量的字节会在构建时在中美之间来回传递,运气好可能 5 分钟能构建完毕,运气不好就可能要等 1 小时了。
那么怎样才能减小这些镜像的大小呢?

首先自然是选一个合适的基础镜像,选项大致有这几种:
  1. python:优点是用它一般不会出现兼容性和工具链缺失的问题,缺点是比较大,超过 1 GB。
  2. bitnami/python:latest:优缺点同上,尺寸稍微小点,但也有 700 MB。此外,没有最新的测试版,只有稳定版。
  3. python:slim:优点是尺寸较小,约 150 MB,缺点是可能缺少一些东西,在 apt-get install 时可能会花更多的时间。
  4. python:alpine:优点是尺寸小,缺点是需要用 apk 取代 apt-get,而且因为使用 musl,与 glibc 存在兼容性问题,导致 pip install 时很多库需要从源码编译,没有构建好的二进制版本,大大增加了构建时间,而且最终的镜像大小可能差不多。
  5. gcr.io/distroless/python3:优点是尺寸小,缺点是没有 shell 和 pip,且最新版本只支持到 Python 3.11。
  6. debianubuntu:用 apt-get install python3 来安装,优点是传说性能较好,尺寸也比 python:slim 大不了多少,缺点是要花更多的构建时间,且版本更新不及时,无法使用最新的 Python 版本。
  7. debianubuntu:自行编译 Python 源码,这个其实和前三种干的事差不多,构建时间非常长,但是看不到明显的收益。
基于尺寸、构建时间和能选择版本的考量,python:slim 是最佳的选择。

然后就是分层构建了,基本上遵循这样的一个流程:
FROM python:slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends ...

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app /app
越不常改动的部分放在越前面执行,这样可以最大限度地利用构建缓存来缩短构建时间。
可是在 apt-get install 时,基本上都需要安装 build-essentialpython3-dev 才能正常编译许多 Python 库,而装上它们就会使镜像大小增大到 500 MB。

如果要减少空间占用的话,就需要改成这样:
FROM python:slim

COPY requirements.txt .
RUN apt-get update \
    && apt-get install -y --no-install-recommends ...
    && pip install --no-cache-dir -r requirements.txt
    && apt-get uninstall ...
    && rm -rf /var/lib/apt/lists/*

COPY app /app
然而这样会导致一旦需要修改 requirements.txt,就得重新构建整个镜像,花费的时间非常长。

那么有没有解决办法呢?答案是 multi-stage 构建:
FROM python:slim AS base

RUN apt-get update \
    && apt-get install -y --no-install-recommends ...

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


FROM python:slim

COPY --from=base /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/

COPY app /app
如果还依赖一些 C 库的话也需要 COPY 到最终镜像里,路径主要在 /usr/lib/x86_64-linux-gnu//usr/lib/aarch64-linux-gnu/(如果是 ARM 平台)。登到镜像里执行一下,看看缺少什么文件就复制过去吧。如果 so 文件存在仍然报 not found 的错误,一般是这个 so 依赖的其他 so 不存在,可以用 ldd 检查一下。
这样优化后,体积应该能控制在 400 MB 以内了。缺点是如果改了依赖的 C 库,可能要重新检查一下缺少的文件,但这个过程也可以用另一个脚本来检测,然后自动补全。

再进一步,python:slim 中其实也有很多用不到的东西,我们可以直接将 debian:bookworm-slim 作为基础镜像,再把 Python 和它的依赖库 COPY 过去:
FROM python:slim AS base

RUN apt-get update \
    && apt-get install -y --no-install-recommends ...

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


FROM debian:bookworm-slim

COPY --from=base /usr/local/bin/python /usr/local/bin/
COPY --from=base /usr/local/lib/libpython3.12.so.1.0 /usr/local/lib/
COPY --from=base /usr/local/lib/python3.12/ /usr/local/lib/python3.12/

COPY app /app
还是老惯例,登到镜像里执行下试试,缺少啥 C 库就 COPY 进去。
这样一来,体积就不到 300 MB 了。

另一个办法是用 gcr.io/distroless/python3 作为基础镜像:
FROM python:3.11-slim AS base

RUN apt-get update \
    && apt-get install -y --no-install-recommends ...

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


FROM gcr.io/distroless/python3

COPY --from=base /usr/local/lib/python3.11/site-packages/ /usr/lib/python3.11/dist-packages/
COPY app /app
最终大小不到 140 MB。

那么还能不能更进一步呢?毕竟 Go 是可以编译成静态链接,直接用 scratch 镜像来运行的。
要说解决方案也不是没有,Python 可以用 pyinstaller 打包成一个可执行文件,然后再用 staticx 转成静态链接。这样打包的 web 应用大概只要 20 多 MB,和 Go 差不多了。这里有个示例,有兴趣可以试试。
但是这样打包可能存在一些兼容性问题,需要花更多的精力去修改代码和测试,而且需要花费更多的时间去构建,更新时也无法利用历史镜像已缓存的层,所以不是很推荐。

实际上经过 gzip 压缩后(docker pulldocker push 本来就会自动处理压缩),基于 python:slim 的镜像就不到 80 MB 了,继续节省的空间已经不值得为此浪费的更多时间了。

2条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?