1.2GB → 98MB,我的 Docker 镜像瘦身实战记录

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 分析界面示意图(左侧层列表,右侧文件树)

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 倍
  • 扩容时拉镜像慢,业务受影响

更重要的是,瘦身的过程,本质上是理解你的应用到底需要什么的过程。当你开始思考"这个依赖真的需要吗?"、"这个工具运行时能用上吗?",你的镜像会越来越小,你的应用也会越来越健壮。

你的镜像现在多大?有没有遇到过镜像太胖引发的诡异问题?评论区聊聊。

相关推荐
分布式存储与RustFS2 小时前
镜像仓库的层别堆本地盘:用 RustFS 给 Docker Registry 当 S3 后端
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
陈希1232 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
@兽血沸腾3 小时前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
闲云自留地9 小时前
K8s 的记忆大脑!ETCD 集群部署、etcdctl 命令与备份恢复全通关
容器·kubernetes·etcd
讯捷蓝达9 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
爱和冰阔落11 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
lisanmengmeng12 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮13 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹13 小时前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器
孙克旭_13 小时前
k8s基础组件与创建资源
运维·docker·容器·kubernetes