← 返回 构建与部署图解 目录

🐳 Docker命令执行图解

build/pull/run/compose 四大命令内部流程 + 面试速答 · SVG可视化

① docker build 命令执行全链路

docker build = 拍证件照:Dockerfile是拍照指南(每条指令=换一套衣服拍一张),基础镜像是你的素颜照(FROM),每条RUN/COPY指令就像化妆+换衣服+拍照,每拍一张就是一层(Layer)。拍完冲洗出来就是最终镜像——一本包含所有照片的相册。而且如果只换了外套(改了某条指令),之前的素颜、化妆、发型照片不用重拍(缓存复用),只从换外套开始重新拍。
📄 Dockerfile FROM openjdk:17 WORKDIR /app COPY pom.xml . RUN mvn dependency:resolve COPY src ./src RUN mvn package EXPOSE 8080 ENTRYPOINT ["java","-jar"] 🔄 Build过程(逐条执行) Layer 0: FROM openjdk:17 → 拉取基础镜像(如果本地没有) Layer 1: WORKDIR /app → 设置工作目录(无文件变化,只改元数据) Layer 2: COPY pom.xml → pom.xml从宿主机复制进来(新增文件层) Layer 3: RUN mvn dependency:resolve → 下载依赖(生成.m2目录,层较大!) Layer 4: COPY src → 源码复制(如果只改了代码,Layer0-3用缓存!) Layer 5: RUN mvn package → 编译打包(生成target/*.jar) EXPOSE + ENTRYPOINT → 只改元数据(端口、启动命令),无文件变化 📦 最终镜像 Layer 0: openjdk:17 (400MB) Layer 1: WORKDIR (0KB) Layer 2: pom.xml (5KB) Layer 3: dependencies (200MB) Layer 4: src (50KB) Layer 5: target/*.jar (30MB) Layer 6: EXPOSE+ENTRYPOINT 总计 ~630MB 7层 UnionFS 叠加 ⚡ Layer0-3 缓存复用!
图1:docker build全链路 — 逐条执行Dockerfile,每条指令生成一层
Dockerfile — Spring Boot项目
📁 project
Dockerfile
pom.xml
📁 src
.dockerignore
# 第一阶段:构建(Maven打包) FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:resolve # 先下载依赖(缓存层!) COPY src ./src RUN mvn package -DskipTests # 再编译打包 # 第二阶段:运行(只保留jar) FROM eclipse-temurin:17-jre-alpine # 只用JRE,更小 WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # ↑ 多阶段构建:最终镜像只有JRE+jar = ~200MB # ↑ 单阶段的话:JDK+Maven+.m2+jar = ~800MB
Dockerfile指令顺序很重要!把变化少的指令放前面(如FROM、COPY pom.xml、RUN下载依赖),变化多的放后面(如COPY src、RUN编译)。因为Docker的缓存是从第一条变化的指令开始,后面全部重建——如果pom.xml没变,依赖下载那层就能用缓存,节省几分钟!

1.1 构建缓存机制

✅ 只改了代码(src/) FROM ✅ 缓存 COPY pom ✅ 缓存 RUN mvn dep ✅ 缓存 COPY src ❌ 重建 RUN mvn pkg ❌ 重建 ⏱ 只需重新编译打包 ⚠️ 改了pom.xml(加依赖) FROM ✅ 缓存 COPY pom ❌ 重建 RUN mvn dep ❌ 重建 COPY src ❌ 重建 RUN mvn pkg ❌ 重建 ⏱ pom一变,后面全部重建!
图2:构建缓存 — 从第一条变化的指令开始,后面全部重建

② docker pull 命令执行全链路

docker pull = 网购收货:Docker Hub就像淘宝商城(全球最大的镜像仓库),你要的镜像是商品,本地仓库是你家储物间。pull的时候Docker先查储物间有没有,没有就去商城下单。而且Docker特别聪明——镜像被分成多个快递箱(Layer),每个箱子独立运输。如果你之前pull过一个老版本(共用了一些Layer),那些箱子不用重复运,只运变化的新箱子!
$ docker pull nginx:latest ① 查本地仓库 → 没有 ② 请求Docker Hub Registry ③ 逐层下载(分Layer传输) Layer a1b2: ✅ 已有(共享) Layer c3d4: ⬇ 下载中... ☁️ Docker Hub registry.hub.docker.com nginx:latest manifest Layer列表 + digest校验 📁 本地仓库 /var/lib/docker/overlay2/ Layer a1b2 ✅ 已有 Layer c3d4 ⬇ 新下载
图3:docker pull — 分层下载,已有的层不重复传输
Terminal — docker pull执行过程
📁 project
Terminal
$ docker pull nginx:latest latest: Pulling from library/nginx # ↓ 每一行对应一个Layer a2abf6c4d29d: Pull complete # Layer 1 - 基础系统 a9edb18cadd1: Pull complete # Layer 2 - nginx包 589260d5d8d5: Pull complete # Layer 3 - 配置文件 b653c091c030: Pull complete # Layer 4 - 入口脚本 1ec0a2e9240a: Pull complete # Layer 5 - 暴露端口 5d5c8e5b9e0e: Already exists # ⚡ 这层本地已有!跳过下载 Digest: sha256:abc123... # 镜像校验码 Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest

③ docker run 命令执行全链路

docker run = 住进精装房:镜像是毛坯房的设计图(只读的),run命令就像按照设计图快速搭建一套装修好的房子(容器=可写的运行实例)。-p端口映射就像开门(外面的人从大门进到你家),-v挂载就像共享储物间(你和隔壁邻居共享一个仓库,东西不会因为房子拆了就丢),--name就像给房子挂门牌号
$ docker run -d -p 8080:8080 --name myapp -v /data:/app/data myapp:latest ① 检查本地有没有镜像 → 有/没有就pull ② 创建容器(Image → Container) 📦 镜像层(只读) Layer 0: JDK Layer 1: app.jar Layer 2: config 🔒 只读共享 ✏️ 容器层(可写) 运行时日志 临时文件 进程写入 📝 容器独有,删了就没了 + 💾 Volume(持久化) -v /data:/app/data 宿主机 /data 目录 ↔ 容器 /app/data 🏠 数据不会随容器消失! 🌐 端口映射 -p 8080:8080 宿主机:8080 → 容器:8080 外面能访问了!
图4:docker run — 镜像→容器,加上可写层+Volume+端口映射
Terminal — docker run常用参数
📁 project
Terminal
# 基础运行 $ docker run myapp:latest # 生产常用 — 后台运行 + 端口 + 名字 + 重启策略 $ docker run -d -p 8080:8080 --name myapp --restart=always myapp:latest # ↑ 后台 ↑ 端口映射 ↑ 容器名 ↑ 崩溃自动重启 # 挂载数据 + 环境变量 $ docker run -d -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ # 数据持久化 -e MYSQL_ROOT_PASSWORD=123456 \ # 环境变量 --name mysql \ mysql:8.0 # 交互式进入容器 $ docker run -it ubuntu:22.04 /bin/bash # ↑ 交互式+终端 # 查看运行中的容器 $ docker ps # 查看所有容器(包括停止的) $ docker ps -a # 查看容器日志 $ docker logs -f myapp

④ docker compose 命令执行全链路

docker compose = 团购套餐:单独docker run就像逐个下单——先点Java应用、再点MySQL、再点Redis……每个都要写一长串参数。docker compose就像团购套餐——一份docker-compose.yml(套餐菜单)搞定所有服务,docker compose up一条命令全部一起上,还自动帮它们连网络、配依赖(先启动数据库再启动应用)。
📄 docker-compose.yml services: app: (Spring Boot) mysql: (MySQL 8.0) redis: (Redis 7) networks: my-net volumes: db-data, redis-data docker compose up -d 一条命令启动全部! MySQL 8.0 Port: 3306 Volume: db-data depends_on: 无 ① 先启动! Network: my-net Redis 7 Port: 6379 Volume: redis-data depends_on: 无 ② 同时启动! Network: my-net Spring Boot Port: 8080:8080 Link: mysql, redis depends_on: mysql,redis ③ 最后启动! Network: my-net
图5:docker compose — 一条命令启动多容器,自动管理网络和依赖
docker-compose.yml — Spring Boot全栈项目
📁 project
docker-compose.yml
Dockerfile
version: '3.8' services: # 应用服务 app: build: . # 用当前目录的Dockerfile构建 ports: - "8080:8080" # 宿主机:容器 environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/mydb # ↑ 用服务名mysql代替IP!Docker DNS自动解析 - SPRING_REDIS_HOST=redis depends_on: - mysql # 先等mysql启动 - redis networks: - my-net # 数据库服务 mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=123456 - MYSQL_DATABASE=mydb volumes: - db-data:/var/lib/mysql # 数据持久化! networks: - my-net # 缓存服务 redis: image: redis:7-alpine volumes: - redis-data:/data networks: - my-net volumes: db-data: # 声明命名卷 redis-data: networks: my-net: # 所有服务同一网络

⑤ 网络数据与面试速答

5.1 Docker网络模式

网络模式 说明 使用场景
bridge 默认模式。容器通过虚拟网桥通信
端口映射到宿主机
大部分Web应用
host 容器直接用宿主机网络
不需要端口映射
需要高性能网络
none 容器没有网络 安全隔离、离线计算
overlay 跨主机容器通信 Docker Swarm/K8s集群

5.2 Volume vs Bind Mount

💾 Named Volume(推荐) -v db-data:/var/lib/mysql ✅ Docker管理 /var/lib/docker/volumes/ ✅ 删容器数据不丢 ✅ 可在compose中声明 ✅ 适合数据库持久化 📁 Bind Mount(开发用) -v /home/user/code:/app ⚠️ 挂载宿主机指定目录 ⚠️ 代码热更新(改代码容器即时生效) ⚠️ 依赖宿主机目录结构 ⚠️ 适合开发环境挂载代码
图6:Volume vs Bind Mount — 生产用Named Volume,开发用Bind Mount

5.3 面试高频问题

docker build的缓存机制是怎样的?如何优化?

缓存机制:Docker逐条执行Dockerfile,每条指令生成一层。如果某条指令的上下文没变,就使用缓存,跳过执行。但一旦某条缓存失效,后面所有指令都会重建,不再使用缓存。

优化策略

1. 变化少的指令放前面:FROM→COPY pom.xml→RUN下载依赖 → COPY源码→RUN编译

2. 多阶段构建:构建阶段用Maven/JDK,运行阶段只用JRE+jar,镜像从800MB→200MB

3. .dockerignore:排除node_modules、.git等不需要的文件,加速COPY

4. 合并RUN指令RUN apt-get update && apt-get install -y xxx,减少层数

镜像和容器有什么区别?

镜像(Image):只读的模板,包含运行应用所需的一切(代码、运行时、库、配置)。就像面向对象中的

容器(Container):镜像的运行实例,在镜像层之上加了一个可写层。就像面向对象中的对象

关系:一个镜像可以创建多个容器,就像一个类可以实例化多个对象。容器删了可写层就没了,但镜像还在。

docker compose的depends_on能保证数据库就绪吗?

不能!depends_on只保证启动顺序(先启动MySQL容器,再启动App容器),但不保证MySQL真正就绪(可能容器启动了但MySQL还在初始化)。

解决方案

1. 健康检查healthcheck + depends_on.condition: service_healthy

2. 应用层重试:Spring Boot连接失败自动重试(spring-retry)

3. wait-for-it.sh:入口脚本等待端口可用再启动应用

ENTRYPOINT和CMD有什么区别?

CMD:容器启动时执行的默认命令,docker run后面加的参数会覆盖CMD。

ENTRYPOINT:容器启动时执行的入口程序,docker run后面加的参数会追加到ENTRYPOINT后面。

# CMD — 可被覆盖 CMD ["java", "-jar", "app.jar"] $ docker run myapp java -jar other.jar # 覆盖CMD # ENTRYPOINT — 参数追加 ENTRYPOINT ["java", "-jar"] CMD ["app.jar"] # 默认参数 $ docker run myapp --server.port=9090 # 追加参数 # 实际执行: java -jar app.jar --server.port=9090

最佳实践:ENTRYPOINT定义入口程序,CMD提供默认参数。这样docker run可以方便地覆盖参数。

如何减小Docker镜像体积?

5个关键策略:

① 多阶段构建:构建阶段用大镜像(JDK+Maven),运行阶段只用小镜像(JRE-alpine),镜像从800MB→200MB

② 用alpine基础镜像:openjdk:17(400MB)→ eclipse-temurin:17-jre-alpine(85MB)

③ 合并RUN指令:多个RUN合成一个,减少层数

④ .dockerignore:排除不需要的文件,加速构建

⑤ 清理缓存RUN apt-get update && apt-get install -y xxx && rm -rf /var/lib/apt/lists/*

📌 速记口诀

docker build:Dockerfile逐条执行→每条指令生成一层→缓存从变化点失效
docker pull:分Layer下载→已有层不重复→Digest校验完整性
docker run:镜像(只读)+容器层(可写)+Volume(持久化)+端口映射(对外)
docker compose:一份YML→一条命令→多容器+网络+依赖全搞定
Volume vs Bind Mount:生产用Named Volume→开发用Bind Mount
镜像瘦身:多阶段构建+alpine镜像+合并RUN+.dockerignore+清理缓存
ENTRYPOINT vs CMD:ENTRYPOINT定入口(追加参数)/ CMD定默认(覆盖参数)

核心三原则速记

Docker三原则:① 多阶段构建减小镜像体积 → ② .dockerignore排除不需要的文件 → ③ 数据持久化用Volume不要存容器内