运维知识
悠悠
2026年2月23日

把Docker镜像从1.2G干到30M,这玩意儿简直是装逼神器!

兄弟们,最近过得咋样?时间过得真快,明天就要开始搬砖了!!!

前两天我在平台上巡检,差点没被气笑。有个新来的开发小伙子,部署了一个简单的Python Web服务,也就是个Flask写的小接口,功能简单得令人发指,结果我看了一眼那个Pod的镜像大小——好家伙,1.2GB!

我当时就去群里@他了,我说:“兄弟,你这是把整个Ubuntu操作系统连带桌面环境都打包进去了吗?不知道的还以为你在容器里挖矿呢。”

这其实是现在很多公司的通病。大家都在喊着云原生,喊着微服务,结果微服务拆分得倒是挺细,镜像却一个个肿得像充了气的河豚。磁盘告警是常事,这还不是最要命的,最要命的是CI/CD流水线。每次发版,光是Pull镜像就得在那转圈转半天,老板问为什么发布这么慢,你敢说是网速不好?

咱干运维的,得有绝活。今天也不藏着掖着,给大伙安利个我用了很久的“减肥药”——Docker-Slim(现在好像改名叫SlimToolkit了,但我们还是习惯叫docker-slim)。

这玩意儿有多狠?它能把一个原本几百兆甚至上G的镜像,在不动代码的情况下,直接压缩到原本的几十分之一。我那个1.2G的Python镜像,最后给我压到了30多M,跑起来啥问题没有。

别不信,今天就带大家从头到尾盘一盘这玩意,顺便聊聊这里面的坑。

为什么你的镜像那么肥?

在讲工具之前,我得先吐槽两句。

很多人的Dockerfile写得简直就是“灾难现场”。什么FROM python:3.9起手,这一个基础镜像下去就得800MB+。然后RUN apt-get update && apt-get install vim curl wget git gcc g++ make... 一顿操作猛如虎。

我就想问问,你生产环境的容器里需要gcc吗?需要git吗?你是准备在容器里现场编译内核还是怎么着?

还有那些COPY指令,COPY . /app,不管三七二十一,把本地的.git目录、甚至是那几个G的测试数据、日志文件全拷进去了。这镜像能不胖吗?

虽然现在有了Multi-stage builds(多阶段构建),稍微好点了。但是Python这种语言挺尴尬的。用python:alpine吧,只有50MB,挺香。但是!兄弟们,Python在Alpine上是个大坑啊。因为Alpine用的是musl libc,而很多Python库(比如numpy, pandas, grpc)的whl包是基于glibc编译的。一旦上了Alpine,pip install的时候找不到预编译包,就开始在现场源代码编译。

那一编译起来,少则几分钟,多则半小时,CPU直接拉满,甚至还会因为缺各种头文件报错。最后你为了编译成功,又装了一堆build-baselinux-headers,最后镜像又回到了几百兆。

这时候,Docker-slim这种“黑科技”就派上用场了。它让你既能用Debian/Ubuntu的兼容性,又能享受Alpine的体积。

Docker-slim 是个啥原理?

这东西聪明就聪明在,它不需要你去手动分析依赖。

它的工作原理有点像我们去医院做体检。它会把你的目标镜像先跑起来,启动成一个临时容器。然后,它利用Linux内核的特性(主要是ptrace、inotify这些机制),在旁边“暗中观察”。

它会监控这个容器启动后都干了啥:

  1. 读取了哪些文件?
  2. 加载了哪些动态链接库(.so文件)?
  3. 运行了哪些网络请求?
  4. 用到了哪些环境变量?
  5. Python解释器到底加载了哪几个.py文件?

等监控结束,它就把这些“用到过的”东西提取出来,再塞到一个极简的空白镜像(scratch)或者极小的底包里,生成一个新的镜像。至于那些没用到的——什么vim、apt、man手册、无用的库文件——统统扔掉。

这就好比搬家,你住了十年,家里一堆破烂。搬家公司(Docker-slim)来了,让你在屋里生活一天,它看你用了牙刷、用了床、用了锅,就把这些搬走,剩下的旧报纸、坏椅子全给你扔垃圾堆。

简单粗暴,但是有效。

实际上手搞一把

光说不练假把式。咱们来个真实的Python例子。

我随手写个简单的Flask应用,这就模拟咱平时最常见的微服务。

代码准备:

搞个 app.py

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello! I used to be a 1GB monster!"

if __name__ == '__main__':
    # 监听所有网卡,端口5000
    app.run(host='0.0.0.0', port=5000)

搞个 requirements.txt

Flask==2.0.1
Werkzeug==2.0.3

构建那个“虚胖”的镜像:

搞个最普通的Dockerfile,故意按很多人的习惯写:

# 直接用官方的大镜像,这玩意儿基于Debian,贼大
FROM python:3.9

WORKDIR /app

# 先把依赖装了
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 把代码拷进去
COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

构建一下:

docker build -t my-fat-flask-app .

构建完我用 docker images 看了一眼。好家伙,885MB

这就离谱。我代码就几行,加上依赖顶多几兆,剩下800多兆全是操作系统的肉。

主角登场:减肥开始

安装docker-slim很简单,如果你是Mac,直接brew一把梭;Linux的话下载个二进制包丢到/usr/local/bin就行,这我就不废话了,大家都会。

核心命令就这一个:build

docker-slim build --target my-fat-flask-app --tag my-slim-flask-app

敲下回车,你会看到终端里开始疯狂刷屏。这感觉就像黑客电影里一样。

它会干这么几件事:

  1. 分析原镜像的元数据。
  2. 启动一个临时容器(Instrumented container)。
  3. 尝试去探测这个容器(Probe),默认它会去调你的HTTP接口(它会自动识别出你在Dockerfile里EXPOSE的5000端口)。
  4. 收集数据。
  5. 生成新镜像。

见证奇迹的时刻:

等它跑完(大概几十秒),你再用docker images看一眼。

REPOSITORY             TAG                 SIZE
my-fat-flask-app       latest              885MB
my-slim-flask-app      latest              28MB

看到没有?从 885MB 变成了 28MB!缩小了30多倍!

这不仅仅是省磁盘空间的问题。你想想,Kubernetes拉取一个28MB的镜像需要多久?一眨眼的事。拉取800多MB呢?网络稍微抖一下,Pod启动就得延时好几分钟。对于扩缩容要求高的业务,这简直是救命的。

而且,安全性也提高了。原来的镜像里有几万个文件,里面可能藏着几百个已知的高危漏洞(CVE)。现在的镜像里只有Python解释器、那几个必须的so库和你的代码。攻击面瞬间小了无数倍。Trivy扫一下,一片清净,安全合规部门的同事看了都得给你递烟。

我们试着运行一下这个瘦身后的镜像:

docker run -p 5000:5000 my-slim-flask-app

访问一下 localhost:5000,输出 "Hello! I used to be a 1GB monster!"。稳如老狗。

坑?那肯定是有的

要是这东西这么完美,那早就统一度量衡了。实际生产中使用,我踩过的坑比吃过的米还多。

坑一:Python的动态加载特性

Docker-slim的核心逻辑是“动态分析”。它得看着你的程序跑,才知道你需要啥。

但Python这语言特别灵活。如果你的代码里有些import是写在函数内部的(Lazy Import),或者用 importlib 动态加载模块,或者根据配置文件去读取某些文件。

举个例子,假设你只在处理 /export_excel 接口的时候才会 import pandas

在Docker-slim构建的过程中,它默认的探针(HTTP Probe)可能只是简单地请求了一下根路径 /,程序返回了个200 OK,Docker-slim就以为完事了。它心里想:“哦,这家伙不需要pandas。”

结果把pandas给删了。

等到上了生产环境,真实用户调了一下导出Excel的功能,程序跑到那一行,傻眼了——ModuleNotFoundError!直接Crash。

怎么破?

这事儿不能怪工具,得怪咱没配置好。你需要告诉它:“兄弟,虽然我刚才没用这几个文件,但你得给我留着。”

你可以用 --include-path 参数强制保留文件或目录:

docker-slim build \
  --include-path /usr/local/lib/python3.9/site-packages/pandas \
  --include-path /app/configs \
  --target my-fat-flask-app

或者,更高级一点,让探针更聪明。

它支持 --http-probe-cmd,你可以自定义探测的命令,比如用curl多调几个接口,覆盖所有的业务逻辑:

docker-slim build \
  --http-probe-cmd "curl http://localhost:5000/ && curl http://localhost:5000/export_excel" \
  --target my-fat-flask-app

甚至,你可以直接暂停在那个临时容器里,手动进去操作一番,让程序把该加载的都加载了,再让它继续生成。这虽然麻烦点,但最稳。

坑二:Web服务启动慢,探针超时

有的Python应用,初始化巨慢,比如要连数据库预热连接池,或者加载一个巨大的机器学习模型(PyTorch/TensorFlow)。Docker-slim启动容器后,默认等个几秒钟就开始探测端口。发现连不上,就报错或者生成了个空壳镜像。

这时候千万别慌,加参数 --http-probe-start-wait,给它点时间缓冲。

docker-slim build --http-probe-start-wait 60 --target my-fat-flask-app

坑三:非Web应用的尴尬

如果你的容器不是个Web Server,而是一个Celery Worker,或者一个定时任务脚本,Docker-slim默认的HTTP探测就废了。

这时候你需要用 --continue-after 参数。比如告诉它,当看到日志里出现 "Worker ready" 这句话时,就认为运行结束了,开始打包。

docker-slim build --continue-after "Worker ready" --target my-worker-image

Xray:透视你的镜像

除了减肥,Docker-slim还有一个特别好用的功能叫 xray

有时候我就想知道,这个第三方镜像到底里面藏了啥猫腻?直接跑:

docker-slim xray --target python:3.9-slim

它会给你列出一份详细的报告,告诉你每一层Layer里都有啥文件,哪些文件变动了,哪些是重复的。这比直接读Dockerfile直观多了。特别是排查那个“因为一层改动导致整个镜像缓存失效”的问题时,Xray简直是显微镜。

为什么我推荐它而不是Alpine?

肯定有人会抬杠:“直接用Alpine作为基础镜像不就行了吗?本来就很小。”

话是没错。Alpine确实只有5MB。但是,前面我也提到了,Alpine + Python = 噩梦

musl libc和glibc的差异能让你怀疑人生。很多C扩展的Python库在Alpine上要么装不上,要么慢得要死,要么跑起来有诡异的Bug(比如DNS解析超时)。

Docker-slim的好处在于,你可以继续使用你熟悉的 python:3.9 (基于Debian) 作为基础镜像

这意味着你拥有了Debian完美的兼容性(glibc),所有的whl包拿来就能用,不需要现场编译。然后通过Docker-slim把体积压得比Alpine还小。

这叫“去其糟粕,取其精华”。既要了Debian的兼容性,又要了比Alpine还小的体积。鱼和熊掌,这次我是真都要了。

生产环境落地建议

虽然这东西好用,但我还是得啰嗦两句生产落地的建议,稳字当头嘛。

  1. 别在生产环境直接跑docker-slim:我的意思是,这个瘦身的过程,应该放在CI/CD流水线的“构建阶段”。开发提交代码 -> Jenkins/GitLab CI 构建原始镜像 -> 运行测试 -> 通过测试后 -> 运行docker-slim生成瘦身镜像 -> 推送到Harbor -> 部署到K8s。
  2. 一定要做验证:瘦身后的镜像,必须经过一轮自动化测试。别盲目自信。万一少了个so库,服务起不来是小事,运行到一半崩了才尴尬。
  3. 保留原始镜像:Harbor里最好保留那份“肥”的镜像一段时间。万一瘦身版的出了诡异问题,咱们能有个回滚的余地,或者方便进去debug。毕竟瘦身后的镜像连Shell可能都没了,你想docker exec进去看日志都做不到。
  4. 调试技巧:如果你发现瘦身后的镜像有问题,但又进不去容器(因为bash都没了)。可以在build的时候加上 --include-shell,强行保留shell,方便你进去排查。查完问题,下次构建再去掉。

总结一下

在这个存储和带宽都要钱,且安全漏洞满天飞的时代,Docker-slim(SlimToolkit)绝对是运维工具箱里不可或缺的神器。

它不是万能的,它需要你对自己的应用行为有一定的了解,需要你花点时间去配置探测参数。但一旦你配置好了,它带来的收益是巨大的:

  • 极速部署:秒级拉取,Python应用再也不怕冷启动。
  • 极致安全:攻击者进来了连个ls命令都敲不了。
  • 节省成本:镜像仓库的存储空间能省下一大笔。

其实很多时候,运维的价值就体现在这些细节上。不是说你会敲几个命令重启服务器就叫厉害,能通过技术手段,在不影响业务的前提下,把基础设施的效率提升十倍,把成本降低十倍,这才是咱们的护城河。

大家回去可以找个非核心的Python业务试试手,要是效果好,记得回来请我喝杯咖啡。

我是老赵,咱们下期见。


公众号:运维躬行录
个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码