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-rmijava-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.gittest 等目录,最终镜像可以从 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 directorynot found

这时候可以用 -slim 版本(如 node:16-slimpython: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 倍
  • 扩容时拉镜像慢,业务受影响

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

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

相关推荐
Sst的头号粉丝1 小时前
Docker——cgroups
运维·docker·容器
❀͜͡傀儡师2 小时前
Docker 部署Datart BI工具完整指南(PostgreSQL 持久化存储)
docker·postgresql·容器
JuckenBoy2 小时前
Linux环境安装SGLang框架运行自选大模型(以Rocky9.7为例)
linux·运维·大模型·qwen·rocky·deepseek·sglang
十巷无终2 小时前
Kali Virtual Machines(虚拟机镜像)安装后问题及解决办法
linux·运维·服务器
l1t2 小时前
解决用docker安装umbra数据库遇到的FATAL:Operation not permitted错误
数据库·docker·容器
架构指南3 小时前
Centos上安装Claude Code报GLIBC_2.27 not found
linux·运维·centos
Predestination王瀞潞3 小时前
4.3.1 存储->微软文件系统标准(微软,自有技术标准):exFAT(Extended File Allocation Table)扩展文件分配表系统
linux·运维·microsoft·exfat·ex4
你有按下913的勇气吗3 小时前
【Agent,RAG,Transform】
linux·运维·服务器
last demo3 小时前
docker存储
运维·docker·容器