Docker 学习篇(四)| Docker的 核心概念
- 第一部分:基础三要素
- 1. 镜像(Image)
- 2. 容器(Container)
- 3. Dockerfile
- 第二部分:启动容器的四件套
- 4. 端口映射(-p)
- 5. 数据挂载(-v)
- 6. 环境变量(-e)
- 7. 资源限制
- 第三部分:容器管理
- 8. 生命周期
- 9. 进入容器(exec)
- 10. 查看日志(logs)
- 11. 清理命令
- 第四部分:编排与组合
- 12. 容器间网络
- 13. 重启策略
- 14. 健康检查(Health Check)
- 15. docker-compose
- `build:` vs `image:` —— 什么时候用哪个?
- 第五部分:构建优化
- 16. 分层缓存
- 17. 多阶段构建
- 18. .dockerignore
- 速查总表
第一部分:基础三要素
1. 镜像(Image)
镜像 = 程序 + 运行环境打包成一个只读文件。
MySQL 镜像里包含:MySQL 程序 + 微型 Linux + MySQL 需要的所有依赖。这个镜像扔到任何装了 Docker 的机器上都能还原出一模一样的运行效果。
镜像和容器的关系:镜像 : 容器 ≈ Word 模板 : 打开的编辑窗口。模板只有一个,用它创建的窗口可以有无数个。
镜像是分层存储的。每条 Dockerfile 指令生成一层:
┌─────────────────────┐ │ COPY demo.jar │ ← 层3:你的 jar ├─────────────────────┤ │ JDK 21 JRE │ ← 层2:运行时 ├─────────────────────┤ │ Alpine Linux │ ← 层1:操作系统 └─────────────────────┘ 最终镜像 = 层1~3 叠加CMD、EXPOSE、ENV 不产生新层,只有 RUN、COPY、ADD 才会。
五个项目都用同一个 JRE 层 → 这一层只存一份,各镜像共享引用。这就是为什么 Docker 比虚拟机省磁盘。
镜像按用途分两类:
| 基础镜像(JDK) | 中间件镜像(MySQL/Redis) | |
|---|---|---|
| 用途 | 作为项目镜像的"底座" | 提供独立网络服务 |
| 存在意义 | 被 Dockerfile 引用(FROM) | 被docker run启动成容器 |
| 不启动容器能用吗? | ✅ 能,已打进项目镜像 | ❌ 不能,不启动则服务不可用 |
记法:基础镜像 = 底座,用了就行。中间件镜像 = 下载了安装包但没安装,必须
docker run才干活。
2. 容器(Container)
容器是用镜像启动出来的运行实例。它是真正在干活的东西——MySQL 容器在存数据,Redis 容器在缓存,你的 blog-server 容器在处理请求。
容器之间文件系统、进程、网络完全隔离。删容器就像删一个 Word 窗口,模板(镜像)不受影响。
3. Dockerfile
你自己的项目没有现成镜像,需要写一份"打包说明书"告诉 Docker 怎么构建。这个说明书就是 Dockerfile。
先回顾没有 Docker 时你怎么部署 Java 项目的:
mvn package → blog.jar → 传到 Linux → java -jar blog.jar有了 Docker 以后,Spring Boot 项目有三种构建方式,本质区别只有一个:编译(mvn package)谁来跑。
方式一:基础 Dockerfile(相当于传统方式)
# FROM = 基于哪个基础镜像(这里用 JDK 21 精简版) FROM eclipse-temurin:21-jdk-slim # COPY = 把宿主机文件复制到镜像里 # 左边:你电脑上的 jar 右边:镜像里的路径 COPY target/*.jar app.jar # EXPOSE = 声明容器监听哪个端口(文档性质,不实际映射) EXPOSE 8080 # CMD = 容器启动时执行的命令 CMD ["java", "-jar", "app.jar"]先手动
mvn package,再把打好的 jar COPY 进镜像。
流程:你电脑 mvn package → docker build → 镜像
最终镜像包含完整 JDK + jar,约 400MB。和传统部署一模一样,只是目的地从 Linux 服务器换成了 Docker 镜像。
方式二:多阶段构建(推荐)
# ===== 阶段1:编译(只用来构建,不进入最终镜像)===== # FROM = 基础镜像,AS builder 给这个阶段起个名字 FROM maven:3.9-eclipse-temurin-21 AS builder # COPY . . = 把当前目录所有文件复制到镜像工作目录 COPY . . # RUN = 在构建过程中执行命令(这里跑 Maven 编译) RUN mvn package -DskipTests # ===== 阶段2:运行(最终镜像只包含这个阶段)===== # FROM = 换个轻量基础镜像,只有 JRE 没有 Maven FROM eclipse-temurin:21-jre-alpine # COPY --from = 从指定阶段复制文件(只取 jar,不要源码和 Maven) # 单模块项目:target/*.jar;多模块项目:blog-bootstrap/target/*.jar COPY --from=builder target/*.jar app.jar # EXPOSE = 声明端口 EXPOSE 8080 # CMD = 启动命令 CMD ["java", "-jar", "app.jar"]编译也进了 Docker,你不需要手动
mvn package。
流程:docker build → 镜像(一条命令,编译+打包全自动)
最终镜像只有 JRE + jar,约 250MB。比方式一多了AS builder+COPY --from=builder两行,省了你手动打包步骤。
为什么方式二比方式一小?省的不是 jar,是底座。
两个方式最终都只拿了一个 jar:
方式一 底座:eclipse-temurin:21-jdk-slim ← JDK ≈ 350MB 方式二 底座:eclipse-temurin:21-jre-alpine ← JRE ≈ 180MBJDK vs JRE:
JDK(Java Development Kit) ├── JRE(Java Runtime Environment)← 运行时,180MB ├── javac 编译器 ← 源码 → .class ├── jdb 调试器 ├── javadoc 文档生成器 └── 其他开发工具... 总约 350MB- 你在 IDEA 里写代码、改代码、编译、调试 → 必须用 JDK
- jar 部署到服务器上,只跑不编译 → JRE 就够了
方式一不分阶段,只能用 JDK 当底座(约 350MB);方式二把编译交给阶段1(Maven 镜像),阶段2 换 JRE 底座(约 180MB)。jar 没变,底座瘦了 170MB。
方式三:Buildpacks(零配置)
不需要 Dockerfile,一行命令:
mvn spring-boot:build-image全自动黑盒生成镜像,约 500MB。上手最快,但命名不灵活、构建过程黑盒、出问题难排查。
三种方式对比:
| 方式一 基础 | 方式二 多阶段 | 方式三 Buildpacks | |
|---|---|---|---|
| 需要 Dockerfile | 需要 | 需要 | 不需要 |
| 需要手动 mvn package | 需要 | 不需要 | 不需要 |
| 最终镜像体积 | ~400MB | ~250MB | ~500MB |
| 相当于传统方式吗 | ✅ 流程一样 | 传统方式的自动化版 | 全自动黑盒 |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
Dockerfile 命名规则:
- 文件名固定叫
Dockerfile(D 大写,无后缀) - 放在项目根目录
docker build默认在当前目录找这个文件- 非要用别的名字也行:
docker build -f 其他文件名 .,但没人这么做
构建时永远加-t起名字,否则 Docker 分配随机 ID:
dockerbuild-tblog-server:latest.# ↑ ↑# 镜像名:标签 构建上下文(Dockerfile 所在目录)标签不写默认为
latest,但它只是个标签名,不代表"最新版本"——Docker 不会自动帮你更新它。
第二部分:启动容器的四件套
4. 端口映射(-p)
dockerrun-p3306:3306 mysql:8.0# ↑ ↑# 宿主机 容器内部默认:不加-p,外面访问不到容器。同一个 Docker 网络下的容器之间不受影响——它们走容器名就能互访,不需要端口映射。
一句话判断:
需要从"宿主机或外部"访问容器 → 加 -p 只是"容器和容器之间"通信 → 不加| 场景 | 要不要映射 | 例子 |
|---|---|---|
| 浏览器访问前端页面 | ✅ 必须 | -p 80:80 |
| IDEA 连 MySQL 调试 | ✅ 开发时需要 | -p 3306:3306 |
| blog-server 连 MySQL | ❌ 不用 | 容器名mysql:3306直接通 |
| blog-server 连 Redis | ❌ 不用 | 容器名redis:6379直接通 |
两个实用技巧:
- 端口冲突了改左边:
-p 3307:3306→localhost:3307连 - 只让本机访问:
-p 127.0.0.1:3306:3306
开发 vs 部署:
开发环境:想连什么映射什么(方便调试) MySQL ✓ Redis ✓ blog-server ✓ blog-ui ✓ 部署上线:只映射前端,其余全关 MySQL ✗ Redis ✗ blog-server ✗ blog-ui ✓ ↑ 多映射一个端口就多一扇攻击面记住一条:容器间互访走容器名,不经过端口映射。只有外面要进来才需要开映射。
5. 数据挂载(-v)
dockerrun-vD:/data/mysql:/var/lib/mysql mysql:8.0# ↑ ↑# 宿主机目录 容器内 MySQL 存数据的地方默认行为:不加-v,数据存在容器的可写层里。容器在数据在,容器删数据跟着消失,没有任何恢复手段。
什么时候不用加:无状态服务或临时测试。比如你跑个hello-world、临时拉个镜像试一下——用完就删,不需要持久化。
什么时候必须加:
- 数据库(MySQL、PostgreSQL 等)
- 文件存储(上传的图片、附件等)
- 任何删容器后还想保留的数据
三种挂载方式怎么选:
| 方式 | 写法 | 适用场景 |
|---|---|---|
| 绑定挂载 | -v D:/data/mysql:/var/lib/mysql | 开发用,你知道路径在哪 |
| 命名卷 | -v mysql-data:/var/lib/mysql | 生产推荐,Docker 管理存储位置 |
| 匿名卷 | -v /var/lib/mysql | 不推荐,删容器后找不到数据 |
宿主机路径必须是绝对路径,不能用相对路径。
推荐:有状态服务必须加(数据库、文件存储),无状态服务不用加。
6. 环境变量(-e)
dockerrun-eMYSQL_ROOT_PASSWORD=root-eMYSQL_DATABASE=blog mysql:8.0默认行为:不加-e,镜像使用内置默认值。不同镜像差异很大:
- MySQL:没有默认密码,不设直接启动失败
- Redis:能启动,但无密码保护(安全风险)
- Nginx:不需要任何变量就能跑
什么时候不用加:镜像默认值刚好满足需求。比如 Nginx 的默认配置就能提供静态文件。
什么时候必须加:默认值不满足需求时。最常见的就是数据库密码、连接地址、服务端口这些。
怎么加更好:
- 敏感信息(密码、密钥)用
env_file单独存放,不要直接写在docker-compose.yml里 - 环境变量只在首次启动时生效。数据已初始化后再改不会自动生效
- 不确定有哪些可用变量?去 Docker Hub 搜镜像名,页面上的 Environment Variables 章节全列出来了
推荐:数据库等必须设的镜像必加,简单服务(Nginx 等)不加也能跑就不加。
7. 资源限制
dockerrun--memory="512m"--cpus="2"...默认行为:不加限制,容器可以无上限使用宿主机所有 CPU 和内存。一个容器写了个死循环,整台机器卡死。
什么时候不用加:本地开发调试。你一个人用,不会跑死循环,多给点资源跑得快。
什么时候必须加:生产环境。多个容器共享一台机器,不设限等于坐等事故——一个服务有问题就能拖垮所有其他服务。
怎么加更好:
- 总内存给宿主机留 20%,其余按服务重要程度分配
- 数据库多给点内存,前端静态服务给 128MB 就够
--memory-swap控制 swap 使用量,生产环境一般设为 memory 的 1.5 倍
推荐:本地开发不用加,上了生产/服务器必须加。
第三部分:容器管理
8. 生命周期
docker start mysql 启动已存在的容器 docker stop mysql 停止 docker restart mysql 重启 docker rm mysql 删除容器(必须先 stop) docker ps 看正在跑的容器 docker ps -a 看所有容器(包括停掉的)停止不是删除。停了还能再启,删了就真没了。
9. 进入容器(exec)
容器跑起来了,想进去看看内部:
dockerexec-itmysqlbash进去后就是一个微型 Linux 终端,可以ls、cat、mysql -uroot -p。用完exit退出。
注意:Alpine 系镜像(标签带alpine)不带bash,只有sh。进去的命令是:
dockerexec-itredissh# alpine 镜像用 sh 而不是 bash不确定镜像有没有 bash?先用sh试试,大多数镜像都有。
10. 查看日志(logs)
容器出错起不来,第一件事看日志:
dockerlogs mysql# 看全部dockerlogs-fmysql# 实时滚动,Ctrl+C 退出dockerlogs--tail50mysql# 只看最近 50 行默认行为:日志用json-file驱动,输出到 Docker 内部文件。没有大小限制,没有自动轮转——一个容器长期运行日志能撑爆磁盘。
生产环境建议配日志上限:
dockerrun --log-opt max-size=10m --log-opt max-file=3...单文件最大 10MB,最多保留 3 个文件,超出自动滚动删除。
推荐:本地开发不管,生产环境必须配日志轮转。
11. 清理命令
Docker 不会自动清垃圾,用久了能吃掉几十 GB:
dockersystem prune# 清理停止的容器、未用的网络、悬空镜像dockervolume prune# 清理未挂载的数据卷dockerbuilder prune# 清理构建缓存默认行为:停止的容器、没打标签的镜像、孤儿数据卷、构建缓存——全都不自动清理,永远堆积。定期手动prune是唯一回收空间的办法。
docker system prune -a可以一次性清理所有未用资源,包括有标签但没被容器使用的镜像。
推荐:每个月跑一次
docker system prune,磁盘告急时跑-a。
第四部分:编排与组合
12. 容器间网络
dockernetwork create blog-netdockerrun--networkblog-net--namemysql...dockerrun--networkblog-net--nameblog-server...默认行为:不加--network,容器被挂到 Docker 默认的bridge网络。这个默认网络不支持 DNS 解析——容器之间只能通过 IP 地址通信,不能用容器名。
也就是说,你得先docker inspect mysql | grep IPAddress查出容器的 IP,再拿这个 IP 去连接。容器重启后 IP 会变,所以基本上没法用。
什么时候默认够用:只跑单个容器,不需要容器间通信。
什么时候必须加自定义网络:两个及以上容器需要互访。
怎么加更好:
- 用
docker-compose——网络自动创建,详见第 15 节 - 手动
docker run的话:docker network create blog-net,然后容器加--network blog-net
限制:不同网络之间默认隔离。一个容器可以加入多个网络(docker network connect),但通常不需要。
本地开发场景区分:
关键看谁连谁——是宿主机上的 IDEA 连容器,还是容器之间互访:
| 场景 | 需要自定义网络? | 为什么 |
|---|---|---|
| IDEA → MySQL 容器 | ❌ 不需要 | localhost:端口走端口映射 |
| blog-server 容器 → MySQL 容器 | ✅ 必须 | 容器间互访需要容器名 DNS |
| 浏览器 → blog-ui 容器 | ❌ 不需要 | localhost:80走端口映射 |
判断方法:连接方在宿主机上(IDEA、浏览器)→ 用
localhost:端口,默认 bridge 即可。连接方在容器里(blog-server)→ 必须自定义网络。
推荐:用 docker-compose,网络自动创建,什么都不用管。手动 docker run 且容器需要互访时,必须建自定义网络。
13. 重启策略
默认行为:不加即--restart no。容器挂了就挂了,Docker 不会自动重启它。Docker Desktop 重启后,所有容器全部停掉,需要手动docker start。
什么时候不用加:本地开发调试。容器挂了你要看错误信息、改配置,自动重启反而干扰排查。
什么时候必须加:生产环境。服务器半夜自动更新重启,不加的话第二天早上所有服务全停。
四个值怎么选:
| 值 | 行为 | 适合 |
|---|---|---|
no(默认) | 不自动重启 | 本地开发、一次性任务 |
always | 挂了自启,Docker 启动自启 | 核心服务(nginx 等) |
unless-stopped | 和 always 一样,但手动 stop 后不自动启动 | 数据库等需要手动停机维护的服务 |
on-failure:5 | 异常退出才重启,最多 5 次 | 任务型容器,正常退出不应该重启 |
推荐:本地开发不加,生产环境统一用
unless-stopped。
14. 健康检查(Health Check)
# 注意:Alpine 镜像不带 curl,需先 RUN apk add --no-cache curl HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1默认行为:不加 HEALTHCHECK,Docker 只看主进程 PID 是否存活——MySQL 进程跑起来了但初始化还没完成,Docker 就认为"正常"。这就是为什么容器状态是running但你连不上。
什么时候不用加:服务启动即就绪(比如 Nginx 几乎没有初始化延迟),或者不依赖健康状态来做启动顺序控制。
什么时候必须加:
- 数据库等初始化较慢的服务(MySQL 首次启动要几十秒)
- 需要控制启动顺序(blog-server 必须等 MySQL 就绪才能启动)
怎么加更好:
interval别太短(至少 10s),否则健康检查本身消耗资源timeout别太短(至少 5s),数据库响应慢一点就被误判为 unhealthy- docker-compose 里的关键用法:
depends_on:mysql:condition:service_healthy# 等健康检查通过才启当前服务推荐:数据库和初始化慢的服务必须加,简单服务(Nginx 等)可不加。
15. docker-compose
把所有docker run的参数(端口、挂载、变量、网络)声明式写成一个文件:
# 注意:端口和路径是示例,实际项目按需改(如 3307:3306 避免冲突)services:mysql:image:mysql:8.0ports:["3306:3306"]volumes:[D:/data/mysql:/var/lib/mysql]environment:MYSQL_ROOT_PASSWORD:rootMYSQL_DATABASE:blogredis:image:redis:7-alpineports:["6379:6379"]docker compose up -d,所有服务全起。
什么时候不用 compose:只跑一个容器,一条docker run就够。
什么时候必须用 compose:一个项目有 2 个及以上的服务。你的 blog 项目有 MySQL + Redis + blog-server + blog-ui = 4 个服务。
Compose 默认帮你做了三件事:
- 自动建网络:名叫
<目录名>_default,service 名 = DNS 域名。DB_HOST: mysql能工作就是因为它 - 自动命名容器:
<目录名>-<service名>-1,不用手动--name - 自动管理启动顺序:
depends_on保证依赖先启动
常用命令:
dockercompose up-d# 启动所有服务dockercompose down# 停止并删除所有容器+网络dockercompose stop / restart / start# 停止/重启/恢复dockercomposeps# 只看当前项目容器dockercompose logs-f# 所有容器日志一起看dockercompose-f指定文件.yml up-d# 用指定 yml 文件启动dockercompose up-d--build# 重新构建镜像再启动dockercompose up-d--force-recreate--build# 强制重建所有容器(改了大配置用这个)改 yml 后重跑
up -d,没变的服务不动,变了的重建。不变的内容跑第二遍不会重启容器。
build:vsimage:—— 什么时候用哪个?
compose 里定义服务来源有两种写法:
# 方式一:build —— 从 Dockerfile 现场编译blog-server:build:./blog-server# 告诉 compose:"去这个目录找 Dockerfile 编译"# 方式二:image —— 用现成的镜像blog-server:image:blog-server:latest# 告诉 compose:"镜像已经存在本地了,直接用"两者本质区别:
build: | image: | |
|---|---|---|
| 镜像从哪来 | 每次从源码编译 | 本地已有或从仓库拉取 |
| 改代码后 | docker compose up -d --build | 先docker build,再docker compose up -d |
| 需要源码 | 是 | 否 |
| 适合场景 | 频繁改代码的本地开发 | 服务器部署、CI/CD、镜像仓库拉取 |
build:的价值:开发体验
一天改几十次代码的时候,docker compose up -d --build一条命令重建+重启,少打一次docker build,迭代快。这就是它存在的理由——语法糖,给本地开发省一个命令。
build:的坑:一个 compose 文件,本地和服务器不通用
compose 文件里有build:,传到服务器上跑docker compose up -d,Docker 找不到源码和 Dockerfile,直接报错。所以用build:的话要维护两套 compose(本地版和服务器版),或者每次部署前手动改。
大团队怎么做:分文件
docker-compose.yml # 共享配置(端口、网络、环境变量) docker-compose.override.yml # 本地覆盖(加 build:),git ignore docker-compose.prod.yml # 生产覆盖(加 image:)docker compose up -d自动合并docker-compose.yml+docker-compose.override.yml,本地有build:,服务器上没有 override 只有image:,各取所需。
个人项目建议:统一用image:
个人项目不值得搞三份 compose 文件。统一用image:,改代码时多打一行docker build,换来的是本地和服务器同一份 compose、不用记规则、不会部署翻车。
推荐:单容器不用 compose,两个及以上服务必用 compose。个人项目统一用
image:,不纠结build:。
第五部分:构建优化
16. 分层缓存
镜像是一层层叠上去的,每条 Dockerfile 指令生成一层:
FROM maven:3.9-eclipse-temurin-21 ← 层1:Maven + JDK COPY pom.xml . ← 层2:依赖描述 RUN mvn dependency:resolve ← 层3:下载依赖 COPY src . ← 层4:源码 RUN mvn package ← 层5:编译没变的层会复用缓存,下次构建只重建变化的层。你改了一行源码,只重跑层4和5,秒级完成。
如果缓存导致问题(比如依赖更新了但缓存没刷新),加--no-cache强制全量重建:
dockerbuild --no-cache-tblog-server:latest.17. 多阶段构建
单阶段构建会把 JDK + Maven + 源码全打进最终镜像,体积 800MB 起。多阶段把"编译"和"运行"分开:
# 阶段1:编译(只用来构建,不进入最终镜像) FROM maven:3.9-eclipse-temurin-21 AS builder COPY . . RUN mvn package -DskipTests # 阶段2:运行(最终镜像只包含这个阶段) FROM eclipse-temurin:21-jre-alpine # 单模块项目:target/*.jar;多模块项目:<启动模块>/target/*.jar COPY --from=builder target/*.jar app.jar CMD ["java", "-jar", "app.jar"]编译工具只留在阶段1,最终镜像只有 JRE + jar,体积砍到 200MB。
什么时候不用:项目不需要自己编译(比如直接运行别人给的 jar 包),一个FROM+COPY+CMD就完事。
什么时候必须用:任何需要编译步骤的项目(Java/Maven、前端/Node 等)。
推荐:只要 Dockerfile 里有编译步骤,就用多阶段构建。
18. .dockerignore
和.gitignore同理,打包镜像时排除不要的文件:
node_modules .git target *.log默认行为:不加.dockerignore,项目目录下所有文件(包括node_modules几万个文件、target几百 MB 构建产物、.git目录)全部发给 Docker 构建引擎。构建又慢又臃肿。
什么时候可以不加:项目极小,只有一个 jar 文件和一个 Dockerfile。其他情况一律加。
什么时候必须加:任何前端项目(node_modules巨大)、任何 Java 项目(target巨大)。
推荐:每个项目都加,养成习惯。
速查总表
| # | 概念 | 如果不配/不加 | 配了以后 | 关键参数 |
|---|---|---|---|---|
| 1 | 镜像 | — | — | — |
| 2 | 容器 | — | — | — |
| 3 | Dockerfile | 没有镜像可用 | 构建出自定义镜像 | -t必须加,否则随机 ID |
| 4 | 端口映射 | 容器完全隔离,外面访问不到 | localhost:端口可访问 | -p 宿主机:容器 |
| 5 | 数据挂载 | 删容器数据跟着销毁 | 数据持久化到宿主机 | -v 宿主机:容器 |
| 6 | 环境变量 | MySQL 等镜像会启动失败 | 按变量初始化服务 | -e KEY=VALUE |
| 7 | 资源限制 | 无上限,能吃满宿主机 | 限制 CPU/内存 | --memory--cpus |
| 8 | 生命周期 | — | — | startstoprm |
| 9 | 进入容器 | — | 进去排查内部状态 | alpine 镜像用sh不是bash |
| 10 | 日志 | 无大小限制,可能撑爆磁盘 | 可配轮转上限 | --log-opt max-size= |
| 11 | 清理 | 垃圾永远堆积,吃掉几十 GB | 手动prune回收 | system prune -a |
| 12 | 容器间网络 | 默认 bridge 只能用 IP;容器间互访需自定义网络 | 容器名即 DNS 域名 | --network;宿主机连容器用localhost:端口,容器互访用容器名 |
| 13 | 重启策略 | 不自动重启,Docker 重启后全停 | 开机自启、挂了自愈 | --restart unless-stopped |
| 14 | 健康检查 | 只看进程死活,不关心服务是否就绪 | 真实验证可用状态 | condition: service_healthy |
| 15 | compose | 手动敲 N 条docker run | 一键全家桶启动 | compose up -d |
| 16 | 分层缓存 | — | 未改层自动复用,秒级构建 | --no-cache可强制全量重建 |
| 17 | 多阶段构建 | 编译工具和源码全打进镜像,体积臃肿 | 最终镜像只含运行环境 | AS builderCOPY --from= |
| 18 | dockerignore | 整个项目目录发给构建引擎,又慢又臃肿 | 排除无用文件,构建飞快 | .dockerignore |