把镜像从 1.2G 压到 180M:一次 Docker 多阶段构建改造
把镜像从 1.2G 压到 180M:一次 Docker 多阶段构建改造
接手一个 Python 服务的部署优化,docker images 一列出来吓一跳:单个镜像 1.2GB,一个 4G 内存的测试机器上光是拉镜像就要三分钟。拆开 Dockerfile 看,问题老生常谈——编译依赖和运行时环境全塞在一个镜像里,pip 缓存、编译工具链、临时文件样样没清。花了一个下午改造成多阶段构建,最终压到 180MB,记录下过程。
改造前的 Dockerfile
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN apt-get update && apt-get install -y gcc libpq-dev
RUN python setup.py build_ext --inplace
EXPOSE 8000
CMD ["python", "app.py"]
几个明显的问题:
- 基础镜像
python:3.11自带一堆用不上的工具,换成-slim能省近 300MB gcc、libpq-dev这些只在编译期需要,装进最终镜像纯属浪费- pip 缓存没清,
/root/.cache/pip动辄上百 MB COPY . .把.git、tests、本地调试文件全带了进去
第一步:换基础镜像
python:3.11 完整版约 1GB,python:3.11-slim 只有 120MB 左右。功能上唯一的差别是完整版预装了编译工具和常用库,而这些在多数 Web 服务里并不会用到。直接换:
FROM python:3.11-slim
这一步就砍掉了一大块。但换来一个坑:某些依赖需要编译,slim 镜像里没有 gcc,pip install 会失败——这正是多阶段构建要解决的问题。
第二步:多阶段构建
核心思路是把"编译"和"运行"拆开:编译阶段用完整环境把 wheel 造好,运行阶段只拷贝成品。
# ---- 构建阶段 ----
FROM python:3.11 AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
# 编译成 wheel,不装到系统里
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ---- 运行阶段 ----
FROM python:3.11-slim
WORKDIR /app
# 只拷 wheel,装完删掉,不留编译工具
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
&& rm -rf /wheels \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
COPY app/ ./app/
EXPOSE 8000
CMD ["python", "app.py"]
关键点:
pip wheel只编译不安装,产物是.whl文件- 运行阶段用
--no-index --find-links=/wheels从本地 wheel 装,不重新下载也不重新编译 --no-cache-dir避免留下 pip 缓存COPY app/ ./app/只复制源码目录,.git和测试文件在.dockerignore里排除
第三步:.dockerignore
这一步常被忽略,但影响很大:
.git
.gitignore
tests/
*.md
__pycache__/
*.pyc
.pytest_cache/
.env
node_modules/
改造前镜像里塞了近 200MB 的 .git 历史和测试数据,COPY . . 一句全带进去了。
踩到的坑
坑一:动态链接库没跟着 wheel 走。 运行阶段装完 wheel 后服务启动报 ImportError: libpq.so.5: cannot open shared object file。psycopg2 编译出来的 wheel 依赖 libpq,但 slim 运行环境里没有这个库。解决方式是运行阶段保留运行时库(不要 libpq-dev 这个开发包,只要 libpq5):
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/*
判断一个库是"编译期"还是"运行期",我的笨办法是编译完先不装,跑起来看报什么错再补。比起一次性把 dev 包全留着,最终也就多几十 KB。
坑二:多阶段构建里的时区。 服务里用了本地时间格式化日志,容器默认 UTC,日志时间整体差 8 小时。slim 镜像里没有 tzdata,TZ=Asia/Shanghai 环境变量也不生效。补上:
RUN apt-get update && apt-get install -y --no-install-recommends tzdata \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone
坑三:构建缓存顺序。 COPY requirements.txt 要放在 COPY app/ 之前,否则每次改一行代码都要重新装一遍依赖,构建时间从 20 秒暴涨到 3 分钟。这个道理都懂,但改造时很容易因为"先把代码拷进去更顺眼"而踩回去。
结果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 镜像大小 | 1.2 GB | 180 MB |
| 冷启动拉取耗时 | ~180s | ~25s |
| 构建耗时(无缓存) | 4min | 3min |
| 构建耗时(改代码后) | 3min | 22s |
镜像变小最直接的好处是部署快,尤其是在多节点同时拉镜像的场景下。其次是安全面缩小——编译工具链、apt 源、pip 缓存这些东西不在运行时镜像里,潜在的攻击面也少了。
小结
多阶段构建不是什么新技巧,但要压到接近理论下限,还是要一个包一个包地看:哪些是编译期需要、哪些是运行期需要、哪些是压根不需要。我的经验是留一个 --no-install-recommends 的习惯,把所有非必需的依赖都挡在门外,再配合定期用 dive 工具看一下镜像分层,基本上能把水分挤干净。
- 点赞
- 收藏
- 关注作者
评论(0)