1.2GB → 98MB,我的 Docker 镜像瘦身实战记录
别急着加节点,先看看你的镜像里到底藏了多少垃圾
你有没有这种经历:写了个挺简单的应用,写完一打包镜像,好家伙,1.2GB。传到公司仓库半小时,拉到生产节点五分钟,磁盘报警天天见。最崩溃的是,明明只改了一行代码,CI/CD 却要跑十分钟------因为那 1GB 多的镜像得从头传一遍。
我以前觉得"镜像大点就大点呗,现在硬盘又不贵",直到有一次客户现场部署,带宽只有 2Mbps,拉一个镜像等了快一小时,甲方领导就站在身后看着,那个尴尬,我到现在都记得。
从那以后,我给自己定了个规矩:任何生产镜像,必须控制在 300MB 以内,能上 100MB 绝不含糊 。今天,我就拿一个真实的 Java 应用(Spring Boot + Vue 前后端分离)开刀,记录下它是怎么从 1.2GB 一路瘦到 98MB 的。
第一刀:先看看你到底胖在哪
拿到一个臃肿的镜像,别急着改 Dockerfile,先诊断。
原始 Dockerfile 长这样,看起来没啥问题:
dockerfile
FROM maven:3.8-openjdk-11
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
EXPOSE 8080
CMD ["java", "-jar", "target/app.jar"]
构建出来的镜像多大?
$ docker images | grep myapp
myapp latest 3f2a7b9c1d4e 2 minutes ago 1.2GB
1.2GB,这还是在生产跑的服务,不是大数据平台。
用 docker history 先看一眼分层情况:
$ docker history myapp:latest
IMAGE CREATED CREATED BY SIZE
3f2a7b9c1d4e About a minute ago CMD ["java" "-jar" "target/app.jar"] 0B
<missing> About a minute ago EXPOSE 8080 0B
<missing> About a minute ago RUN /bin/sh -c mvn clean package -DskipTests 654MB
<missing> 2 minutes ago COPY . . 18.2MB
<missing> 2 minutes ago WORKDIR /app 0B
<missing> 3 weeks ago /bin/sh -c #(nop) CMD ["/usr/local/bin/mav... 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/local/... 0B
<missing> 3 weeks ago /bin/sh -c ln -s /usr/share/maven/bin/mvn /... 0B
<missing> 3 weeks ago /bin/sh -c #(nop) COPY file:a7b078ffa4b534f... 9.77kB
<missing> 3 weeks ago /bin/sh -c #(nop) ENV MAVEN_CONFIG=/root/.m2 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ENV MAVEN_HOME=/usr/share... 0B
<missing> 3 weeks ago /bin/sh -c mkdir -p $MAVEN_HOME && curl -f... 9.83MB
<missing> 3 weeks ago /bin/sh -c #(nop) ENV MAVEN_VERSION=3.8.8 0B
<missing> 3 weeks ago /bin/sh -c apt-get update && apt-get insta... 502MB
<missing> 3 weeks ago /bin/sh -c #(nop) CMD ["jshell"] 0B
<missing> 3 weeks ago /bin/sh -c set -eux; arch="$(dpkg --print-... 256MB
<missing> 3 weeks ago /bin/sh -c #(nop) ENV JAVA_VERSION=11.0.20+8 0B
看到没,基础镜像层 502MB + JDK 层 256MB + Maven 层 9.8MB + 构建层 654MB = 1.2GB。
history 只能看个大概,真想看清每一层里到底有什么,得上 dive 。
$ dive myapp:latest

dive 会把每一层展开给你看,还能标出**"浪费的空间"**。我一眼就发现问题:
- /root/.m2/repository :Maven 下载的所有依赖都在,400 多 MB
- /usr/lib/jvm:完整的 JDK,包含大量调试工具(jdb、jmap 等),运行时根本用不上
- /var/cache/apt:apt 缓存没清理,几十 MB 的垃圾
- 源码目录里的 .git 和测试文件也被 COPY 进来了
结论很清晰:构建环境和运行环境混在一起,且没有任何清理。
第二刀:基础镜像瘦身,Alpine 走起
原始镜像用的是 maven:3.8-openjdk-11,这是一个**"大礼包"镜像**------里面既有 Maven 又有 JDK,还有完整的 Debian 系统。其实运行 Java 应用只需要 JRE,连 Maven 都不需要。
先换基础镜像:
dockerfile
# 第一阶段:用带 JDK 和 Maven 的镜像来编译
FROM maven:3.8-openjdk-11 AS builder
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
# 第二阶段:用轻量级 JRE 镜像运行
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/app.jar .
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
重新构建,镜像大小瞬间从 1.2GB → 392MB。
但还能再瘦:openjdk:11-jre-slim 基于 Debian,300 多 MB。换成 Alpine 版本试试:
dockerfile
FROM openjdk:11-jre-alpine
结果:
myapp:optimized latest d4e5f6g7h8i9 1 minute ago 198MB
198MB!主要原因是 Alpine 基于 musl libc 和 busybox,系统本身只有 5MB 左右 。
第三刀:多阶段构建,把编译工具链全扔掉
现在镜像 198MB,看起来不错了。但仔细看,Alpine 里的 JRE 还是包含了不少运行时不需要的组件,比如 java-rmi 、java-corba 等模块。
真正的进阶玩法是:把 JDK 里的模块拆开,只打包你需要的 。
dockerfile
# 第一阶段:编译 + 自定义 JRE
FROM openjdk:11-slim AS builder
WORKDIR /app
COPY . .
# 编译应用
RUN apt-get update && apt-get install -y maven && \
mvn clean package -DskipTests && \
apt-get remove -y maven && apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
# 使用 jlink 创建最小化 JRE(Java 9+ 支持)
RUN jlink \
--module-path /opt/java/openjdk/jmods \
--add-modules java.base,java.logging,java.sql,java.xml,jdk.unsupported \
--output /custom-jre \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2
# 第二阶段:使用自定义 JRE 运行
FROM alpine:3.18
# 安装 glibc 兼容层(因为 jlink 出来的 JRE 需要 glibc)
RUN apk add --no-cache libc6-compat
# 从 builder 阶段复制自定义 JRE 和应用
COPY --from=builder /custom-jre /jre
COPY --from=builder /app/target/app.jar /app.jar
EXPOSE 8080
CMD ["/jre/bin/java", "-jar", "/app.jar"]
构建完后看大小:
myapp:ultra-slim latest i9j0k1l2m3n4 1 minute ago 98MB
98MB! 从 1.2GB 到 98MB,压缩比超过 90%。
这个过程中最关键的技巧是 jlink ------它能根据你的应用实际用到的 Java 模块,生成一个最小化的 JRE 。比如我的应用只用到了 java.base、java.sql 等几个模块,最终 JRE 只有 40 多 MB。
第四刀:Node.js 应用同理,但要注意这些坑
如果你的项目是 Node.js(比如前端 + Node 后端),思路完全一样,只是工具不同。
原始 Dockerfile 可能长这样:
dockerfile
FROM node:16
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
构建出来轻轻松松 1.5GB+。
多阶段构建版本:
dockerfile
# 第一阶段:构建
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --silent --no-audit --no-fund
COPY . .
RUN npm run build
# 第二阶段:运行
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
配合 .dockerignore 排除 node_modules、.git、test 等目录,最终镜像可以从 1.5GB → 120MB 左右 。
第五刀:还有哪些被你忽略的瘦身点?
1. .dockerignore 是保命符
很多人写了 .gitignore 但忘了 .dockerignore。构建上下文里如果有 node_modules 或 .git(动辄几百 MB),即使最后没 COPY 进镜像,构建过程也会把这些文件传给 Docker daemon,拖慢构建速度 。
一个标准的 .dockerignore:
.git
node_modules
dist
.log
Dockerfile
.dockerignore
README.md
test/
coverage/
.env
.idea
.vscode
2. 清理、清理、再清理
同一层里做的事,记得清理缓存:
dockerfile
# 反例
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # 这一层删了,但上一层还在
# 正例
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
因为 Docker 镜像是分层的,上一层里删掉的文件其实还在------只是被标记为删除,文件数据依然存在于前一层的镜像中 。
3. Alpine 虽好,但不是万能
Alpine 基于 musl libc,和常见的 glibc 不完全兼容。如果你的应用依赖某些预编译的二进制(比如 Oracle 的 JDBC 驱动、某些 Python 的 wheel 包),可能会报 No such file or directory 或 not found 。
这时候可以用 -slim 版本(如 node:16-slim、python:3.9-slim)作为折衷,虽然比 Alpine 大一点,但兼容性好很多。
效果对比:到底省了多少?
| 优化阶段 | 镜像大小 | 压缩比 | 构建时间 | 拉取时间(2Mbps) |
|---|---|---|---|---|
| 原始镜像 | 1.2GB | - | 3分20秒 | 约 80 分钟 |
| 换 slim 基础镜像 | 392MB | 67% | 2分10秒 | 约 26 分钟 |
| 多阶段构建 | 198MB | 83% | 1分50秒 | 约 13 分钟 |
| jlink 自定义 JRE | 98MB | 92% | 2分30秒 | 约 6.5 分钟 |
拉取时间从 80 分钟降到 6.5 分钟------甲方领导再也不用站在身后叹气了。
写在最后:镜像瘦身,不只是省硬盘
很多人觉得镜像大点没啥,云硬盘便宜。但你算笔账:
- 每次 CI/CD 多跑 5 分钟,一天 20 次构建,就是 100 分钟,团队 5 个人,每周浪费 8 小时
- 镜像仓库存储成本翻 10 倍
- 扩容时拉镜像慢,业务受影响
更重要的是,瘦身的过程,本质上是理解你的应用到底需要什么的过程。当你开始思考"这个依赖真的需要吗?"、"这个工具运行时能用上吗?",你的镜像会越来越小,你的应用也会越来越健壮。
你的镜像现在多大?有没有遇到过镜像太胖引发的诡异问题?评论区聊聊。