CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Docker 容器入门/课时/构建与优化
RawBlame
课时目录12 节

Docker 容器入门

  1. 01容器是什么
  2. 02安装与环境
  3. 03Docker CLI 基础
  4. 04镜像与分层
  5. 05编写 Dockerfile
  6. 06构建与优化
  7. 07镜像仓库
  8. 08数据卷与挂载
  9. 09网络
  10. 10Docker Compose
  11. 11容器安全基础
  12. 12综合练习与 K8s 方向

构建与优化

层缓存、减小体积、多阶段构建入门。

35 分钟

构建与优化

这一课你将能做到

  1. 看懂构建输出里的 CACHED,说出「哪些指令会命中缓存、什么情况下缓存会失效」
  2. 用多阶段构建把 Spring Boot 镜像从「源码 + JDK + Maven」瘦身到「纯 JRE + jar」,体积砍掉一半以上
  3. 熟练使用 docker build 的 -t、-f、--build-arg、--no-cache、--platform 参数

为什么要学

同样的 Spring Boot 项目,有人构建要 5 分钟、镜像 800MB;有人构建 30 秒、镜像 250MB。差距来自两点:对层缓存的理解、对镜像内容的取舍。CI 上每次构建多等 4 分钟,一天几十次构建就是几个小时的白等;镜像多 500MB,每次发布就多传 500MB。

这一课解决的问题非常具体:改一行 Java 代码,为什么整个镜像要重建?怎么让 Maven 依赖下载只发生一次?最终的镜像里为什么不需要 Maven 和源码?答案就是本课的两个核心:层缓存与多阶段构建。

先建立图像

构建缓存的判定规则:从上到下逐条指令比对,一旦某层失效,其后所有层全部重建。

# 反例:每次改代码,mvn 依赖全部重新下载
FROM eclipse-temurin:21-jre
COPY . /app                ← 任何文件变了,这层失效
RUN cd /app && mvn package ← 上一层失效 → 这层必然重跑(重新下载依赖)
# 正例:依赖声明先 COPY,代码后 COPY
COPY pom.xml .             ← pom 没变 → 缓存命中
RUN mvn dependency:go-offline  ← 缓存命中,依赖一次都不用重新下
COPY src ./src             ← 只有 src 变了 → 仅这层起失效
RUN mvn package            ← 重跑,但依赖已缓存,快

多阶段构建的分工:

阶段 1 builder:Maven + JDK + 源码 → 产出 jar     (构建完即弃)
阶段 2 runtime:纯 JRE + jar → 最终镜像            (只带走结果)

跟着做

步骤 1:先体验一次缓存命中

用上一课的最小 Dockerfile,连续构建两次:

docker build -t demo-app:1.0 .
docker build -t demo-app:1.0 .     # 第二次几乎全部 CACHED,秒级完成

然后重新 mvn clean package(jar 内容变了),再构建:

mvn clean package -DskipTests
docker build -t demo-app:1.1 .
# 输出:FROM/WORKDIR 层 CACHED,COPY target/... 层重新执行

现象确认:只有「内容变化的层」及其后的层重建,FROM 这种基础层永远命中。

步骤 2:改造 Dockerfile,利用依赖缓存(单阶段版)

前提:你在本地/CI 先 mvn package。如果想让镜像内完成编译,用下面多阶段版;此处先改造 COPY 顺序养成习惯:

FROM eclipse-temurin:21-jre
ENV TZ=Asia/Shanghai
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

改动很小的项目,反复构建基本只重建 COPY 这一层,其余全缓存。

步骤 3:多阶段构建——镜像里自己完成编译

# ---- 阶段 1:构建 ----
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B          # 先拉依赖,pom 不变则永远缓存
COPY src ./src
RUN mvn clean package -DskipTests

# ---- 阶段 2:运行 ----
FROM eclipse-temurin:21-jre
ENV TZ=Asia/Shanghai
WORKDIR /app
COPY --from=builder /build/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

构建与验证:

docker build -t demo-app:1.2 .
docker run -d --name demo2 -p 8081:8080 demo-app:1.2
curl http://localhost:8081/actuator/health

步骤 4:对比镜像体积

docker images | findstr demo-app

典型结果:单阶段用 maven 基础镜像的版本约 700MB+;多阶段最终镜像 200~300MB(只有 JRE + jar)。Maven、JDK 编译器、.m2 缓存、源码全部留在了 builder 阶段,不会进入最终镜像。

步骤 5:常用构建参数

docker build -t demo-app:1.3 .                     # -t 打标签
docker build -f Dockerfile.prod -t demo-app:prod . # -f 指定其他 Dockerfile
docker build --build-arg JAR_VERSION=1.2 -t demo-app:x .  # 传构建期变量
docker build --no-cache -t demo-app:fresh .        # 全量重建(怀疑缓存脏了时用)
docker build --platform linux/amd64 -t demo-app:amd64 .   # 指定目标平台

Dockerfile 里配合 ARG 使用(注意 ARG 与 ENV 的区别:ARG 只在构建期有效,ENV 会带进运行期):

ARG JAR_VERSION=unknown
LABEL version=${JAR_VERSION}

步骤 6:进阶方向——Spring Boot 分层 jar(了解即可)

Spring Boot 3 支持把 fat jar 拆成「依赖层 + 代码层」,让代码变更只重建 KB 级的层:

java -Djarmode=tools -jar app.jar extract --layers --destination extracted

配合 Dockerfile 把各层分别 COPY。中小项目收益有限(依赖层本来就不常变),团队项目规模上来后再引入。本课把它作为方向认知,不作为必做项。

讲清楚要点

  1. 缓存判定的粒度是指令 + 输入内容:RUN 看指令字符串本身是否变过;COPY/ADD 看被复制文件的内容校验和。父层失效,其后全部失效——所以「变化少的写前面,变化大的写后面」是铁律。

  2. 多阶段构建的减脂原理:最终镜像只由最后一个阶段生成,COPY --from=builder 只拷贝指定文件。构建工具、源码、缓存全部留在废弃阶段。

  3. 基础镜像选型梯度(Spring Boot 运行时):eclipse-temurin:21-jre(约 300MB,稳)→ 21-jre-alpine(约 180MB,注意 musl)→ 自定义 jlink/jlink 精简运行时(更小,成本高)。先跑稳,再减脂。

  4. 合并 RUN 减少层数:同层内下载+解压+删除要写进同一条 RUN,否则「下载层」依然留在镜像里:

    RUN apt-get update && apt-get install -y curl \
      && rm -rf /var/lib/apt/lists/*
    
  5. --platform 是跨架构部署刚需:Apple Silicon 开发机构建的 arm64 镜像,推给 amd64 云服务器会报 exec format error。CI 构建一律显式指定 --platform linux/amd64,或用 buildx 一次产出多架构镜像。

你可能会懵的地方

  • 我明明什么都没改,为什么这次没走缓存? 缓存自上而下判定,任何上游层失效都会传染。常见触发:COPY . . 把日志/临时文件也拷了(内容变了)→ 全链失效。解法:精确 COPY + 完善的 .dockerignore。

  • 在镜像里构建和在 CI 里构建,选哪个? 镜像内多阶段构建的好处:任何有 Docker 的机器都能一键构建,环境绝对一致;坏处:构建期需要联网拉依赖。CI 构建产物再 COPY 进镜像的方案在流水线成熟的公司更常见。两种都必须会写。

  • --no-cache 什么时候用? 怀疑缓存污染(比如依赖仓库发布过同版本号的新包)或安全要求强制拉最新基础镜像时。日常不要用,它让你白付所有下载成本。

  • 为什么我的构建上下文有 2GB? docker build . 会把目录整个发给 daemon。项目里有前端 node_modules、日志、数据备份文件都会被算进去。.dockerignore 一行 logs 就能砍掉大半。

  • 镜像还能再小吗? 能,但有边际成本:jlink 定制运行时、distroless 基础镜像、GraalVM native image(编译成原生可执行文件,启动毫秒级,但构建复杂、反射兼容要逐个处理)。先确认「小」真的是你的瓶颈再上。

错法对照

错法问题正解
COPY . . 放在 Dockerfile 开头任何文件变动导致全量重建精确 COPY,产物类放最后
依赖下载和编译写进同一个 RUNpom 未变也要重新下载依赖拆成「COPY pom + 拉依赖」「COPY src + package」两段
单阶段用 JDK/Maven 镜像直接跑应用镜像 700MB+,攻击面大多阶段构建,运行镜像只含 JRE + jar
RUN 里下载和删除写两条删除发生在新层,体积不减同一条 RUN 内 && rm -rf
Apple Silicon 构建完直接部署 x86 服务器运行报 exec format error--platform linux/amd64 或 buildx 多架构
每次构建都加 --no-cache全量下载,CI 时间翻倍只在怀疑缓存污染时使用

卡点急救

  1. 多阶段 COPY --from 报找不到文件:检查源路径是 builder 阶段内的绝对路径(/build/target/app.jar),并确认 mvn package 的 finalName 与之一致。
  2. 镜像内 Maven 拉依赖超时:构建环境网络问题。可在 builder 阶段挂国内 settings.xml:COPY settings.xml /root/.m2/settings.xml(阿里云 Maven 镜像仓库配置)。
  3. 构建时报 failed to solve: ... exec format error:基础镜像架构与当前机器不符,给 FROM 相关镜像或整个 build 加 --platform。
  4. 缓存永远不生效:确认 docker build 传的是同一个上下文目录;BuildKit 下若用 --mount=type=cache 的缓存被清也会重建。
  5. 构建产物与预期不符:docker history 镜像名 逐层核对哪条指令产生了意外内容,比盲改 Dockerfile 快。

面试常问

  • Docker 构建缓存的工作机制? 逐层判定:RUN 比对指令文本,COPY/ADD 比对文件内容哈希;某层失效则其后全部重建。优化手段是合理排序(稳定的在前)与最小化 COPY 范围。

  • 什么是多阶段构建,解决什么问题? 一个 Dockerfile 定义多个 FROM 阶段,构建阶段包含编译工具链,最终阶段只 COPY 构建产物。解决「编译环境污染运行镜像」的问题,显著减体积与攻击面。

  • 如何给 Spring Boot 应用做镜像瘦身? 多阶段构建(去掉 Maven/JDK/源码)→ 运行镜像选 JRE 而非 JDK → 基础镜像选 slim/alpine → 合并 RUN 并同层清理缓存 → .dockerignore 缩小上下文 → 进阶用 Spring Boot 分层 jar 或 jlink。

  • CI 流水线里构建慢怎么排查? 看三处:构建上下文是否过大(.dockerignore);依赖是否反复下载(依赖层未命中缓存);是否用了 --no-cache。远程缓存(registry 缓存后端)可作为进阶方案。

自测三问

  1. 下面两条指令为什么第一条放前面能加速构建:COPY pom.xml . 与 COPY src ./src?
  2. 多阶段构建后镜像从 700MB 降到 250MB,减掉的 450MB 具体是什么东西?
  3. ARG 和 ENV 有什么区别?--build-arg 传的值会留在最终镜像里吗?

小结一句话

构建优化的全部秘密是「变化少的层放前面 + 只把产物带进最终镜像」——层缓存省时间,多阶段构建省体积,两者共同决定你的 CI 与发布效率。

延伸

  • 多阶段构建官方文档
  • Spring Boot Docker 分层官方文档
  • 下一课预告:镜像仓库——把构建好的镜像推上 Docker Hub / 阿里云 ACR,让任何机器一拉即用。
加载本节练习…
← 上一课编写 Dockerfile下一课 →镜像仓库