ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Dockerfile 镜像从 1G 优化到 100M:多阶段构建与瘦身实战

Dockerfile 镜像从 1G 优化到 100M:多阶段构建与瘦身实战 CI 上构建一个 Spring Boot 镜像docker images 一看 1.2G。推送到仓库要几分钟节点拉取要几分钟存三个版本就吃掉几个 G 磁盘。更糟的是镜像里带着 Maven、JDK 完整工具链、源码和 .git 目录——这些东西运行时根本用不到却全部暴露在镜像里既是体积负担也是安全面。这篇用一个真实项目演示怎么把镜像从 1G 压到 100M 以内覆盖基础镜像选型、多阶段构建、.dockerignore、层缓存优化、以及最终用 distroless/scratch 收尾。Java、Node、Go 三个例子都给。先看清镜像里装了什么优化前先测量别凭感觉。两条命令# 看镜像总体积 docker images myapp # 看每一层的体积找出大头 docker history myapp:1.0 --humandocker history 输出节选IMAGE CREATED SIZE a1b2c3d4e5f6 2 minutes ago 420MB ← COPY . . 把整个项目拷进去了 b2c3d4e5f6a7 2 minutes ago 350MB ← RUN mvn package 构建产物Maven缓存 c3d4e5f6a7b8 5 minutes ago 400MB ← FROM maven:3.9-eclipse-temurin-17 基础镜像一眼看出问题基础镜像用了带完整 JDK Maven 的 maven: 镜像400M又把源码和构建缓存都留在了最终镜像里。更精细的分析可以用 dive开源工具dive myapp:1.0它会逐层展示文件树和每层增量标出重复和被覆盖的文件定位哪一层偷偷变大非常好用。优化一换基础镜像收益最大的一步最终运行镜像不需要构建工具链甚至不需要完整 JDK。按运行时选最小的基础镜像应用类型推荐基础镜像大约体积Java只需 JREeclipse-temurin:17-jre-alpine~120MBJava需 glibc 兼容eclipse-temurin:17-jre-jammy~250MBNodenode:22-alpine~130MBGo静态编译scratch / alpine~10MB / ~8MB静态文件/前端nginx:alpine~25MB极致安全distrolessgcr.io/distroless/java17~200MB 但无 shell光把 FROM maven:...含 JDKMaven~400M换成 FROM eclipse-temurin:17-jre-alpine~120M就省了近 300M。优化二多阶段构建把构建和运行分离核心思想构建阶段用带工具链的大镜像编译打包运行阶段只从构建阶段拷贝最终产物jar / 二进制 / dist 目录工具链和源码全部丢弃。Java 示例# ---- 阶段1构建 ---- FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build # 先只拷 pom利用层缓存依赖不变时不重新下载 COPY pom.xml . RUN mvn dependency:go-offline -B # 再拷源码编译 COPY src ./src RUN mvn package -DskipTests -B # ---- 阶段2运行 ---- FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 只从 builder 拷最终 jar COPY --frombuilder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]最终镜像 JRE 基础~120M 你的 jar几十 MMaven、源码、.git 全不在里面。1.2G → ~180M。Node 示例FROM node:22-alpine AS builder WORKDIR /build COPY package*.json ./ RUN npm ci # 按 lock 文件装含 devDependencies构建需要 COPY . . RUN npm run build # 产出 dist/ FROM nginx:1.27-alpine COPY --frombuilder /build/dist /usr/share/nginx/html EXPOSE 80前端最终镜像就是 nginx 静态文件~25M。node_modules往往几百 M完全不带进运行镜像。Go 示例极致scratchFROM golang:1.22-alpine AS builder WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . # CGO_ENABLED0 产出纯静态二进制 RUN CGO_ENABLED0 GOOSlinux go build -ldflags-s -w -o /app/server . FROM scratch COPY --frombuilder /app/server /server ENTRYPOINT [/server]scratch 是空镜像最终只有你的静态二进制-ldflags-s -w 去掉符号表再瘦一圈。一个 Go 服务最终 ~10-20M。优化三.dockerignore别让垃圾进构建上下文COPY . . 会把构建上下文里所有文件发给 Docker daemon包括 .git、node_modules、target、日志。即使多阶段构建最终不拷它们它们也进了构建上下文、拖慢构建、还可能进中间层。项目根目录加 .dockerignore.git .gitignore node_modules target dist *.log *.md .dockerignore Dockerfile docker-compose*.yml .env .idea .vscode效果构建上下文从几百 M 降到几 MCOPY . . 那层也不再包含垃圾文件。这一步对构建速度提升非常明显。优化四合并 RUN 层 清理缓存每个 RUN 产生一层层里删除的文件不会减小镜像只是标记删除数据还在下层。所以安装和清理要放在同一个 RUN里# 错误分两层apt 缓存留在第一层镜像变大 RUN apt-get update apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # 正确同一层内装完即清 RUN apt-get update apt-get install -y --no-install-recommends curl \ rm -rf /var/lib/apt/lists/*Alpine 同理RUN apk add --no-cache tzdata curl--no-cache / --no-install-recommends 从源头减少安装量。npm 场景# 构建阶段用 npm ci含 dev 依赖 RUN npm ci # 如果运行阶段确实需要 node_modules用 production 安装 RUN npm ci --omitdev npm cache clean --force优化五层缓存顺序加速构建间接减少重复层Docker 按顺序执行指令某层变化会导致其后所有层缓存失效。把变化频率低的放前面# 依赖清单变化少 → 放前面缓存命中率高 COPY package*.json ./ RUN npm ci # 源码变化频繁 → 放后面 COPY . . RUN npm run build如果先 COPY . . 再 npm ci那每次改一行代码 npm ci 都要重跑。顺序对了改代码只重跑构建不重装依赖CI 时间从几分钟降到几十秒。优化六运行用户与安全收尾顺手把安全也做了不减小体积但属于最佳实践FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --frombuilder /build/target/*.jar app.jar # 用内置的非 root 用户运行 USER 1000:1000 EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]避免容器以 root 运行降低逃逸风险。优化效果对比同一个 Spring Boot 项目各阶段体积阶段做法镜像体积起点FROM maven COPY . 构建单阶段~1.2GB换基础镜像运行阶段改 temurin:17-jre-alpine~400MB多阶段构建只拷 jar~180MB.dockerignore上下文干净~180MB构建更快合并 RUN 清理去掉 apt/npm 缓存~170MBGo 项目用 scratch静态二进制~15MBJava 项目到 ~170-180M 基本是 alpine JRE 的下限JRE 本身就 100M。要再小只能换 GraalVM native-image编译成原生二进制~50M 但构建慢、有反射配置成本一般业务项目到 180M 就够了。验证与持续检查构建后复核docker images myapp --format {{.Repository}}:{{.Tag}} {{.Size}} docker history myapp:1.0 --human | headCI 里可以加一道体积门禁超过阈值直接失败防止镜像悄悄变胖SIZE$(docker image inspect myapp:1.0 --format{{.Size}}) if [ $SIZE -gt 300000000 ]; then echo 镜像超过300MB检查构建; exit 1; fi小结镜像瘦身的杠杆按收益排序多阶段构建 换小基础镜像 .dockerignore 合并 RUN 清缓存 层缓存顺序。多阶段构建把工具链和源码挡在运行镜像外是收益最大的一步运行镜像只装运行时Java 用 -jre-alpine、前端用 nginx:alpine、Go 用 scratch.dockerignore 保证构建上下文干净提速又减层安装和清理放同一个 RUN避免删了但没瘦用 docker history 和 dive 测量用 CI 体积门禁防回归一套做下来1G 的镜像压到 100-200M 是常规操作Go/静态类压到几十 M 也不难。镜像小了推送快、拉取快、省磁盘、攻击面也小是性价比极高的优化。
返回列表