← 返回 构建与部署图解 目录
🐳 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项目
# 第一阶段:构建(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执行过程
$ 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常用参数
# 基础运行
$ 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全栈项目
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不要存容器内