|
|
@@ -1284,7 +1284,7 @@ dockerClient.<命令方法>(<参数>)
|
|
|
|
|
|
| 方法 | 对应命令 | 说明 |
|
|
|
|------|----------|------|
|
|
|
-| `pingCmd()` | `docker ping` | 检测 Docker daemon 连接是否正常 |
|
|
|
+| `pingCmd()` | 无对应 CLI(HTTP `/_ping`) | 检测 Docker daemon 连接是否正常 |
|
|
|
| `infoCmd()` | `docker info` | 获取 Docker daemon 系统信息 |
|
|
|
| `versionCmd()` | `docker version` | 获取 Docker 版本信息 |
|
|
|
| `authCmd()` | — | 进行 Registry 认证 |
|
|
|
@@ -1367,7 +1367,14 @@ dockerClient.<命令方法>(<参数>)
|
|
|
| `createVolumeCmd()` | `docker volume create` | 创建数据卷 |
|
|
|
| `removeVolumeCmd(name)` | `docker volume rm` | 删除数据卷 |
|
|
|
|
|
|
-#### 八、网络(Network)
|
|
|
+#### 八、清理(Prune)
|
|
|
+
|
|
|
+| 方法 | 对应命令 | 说明 |
|
|
|
+|------|----------|------|
|
|
|
+| `pruneCmd(PruneType.CONTAINERS)` | `docker container prune -f` | 清理已停止的容器 |
|
|
|
+| `pruneCmd(PruneType.IMAGES)` | `docker image prune -f` | 清理悬空(dangling)镜像 |
|
|
|
+
|
|
|
+#### 九、网络(Network)
|
|
|
|
|
|
| 方法 | 对应命令 | 说明 |
|
|
|
|------|----------|------|
|
|
|
@@ -1465,5 +1472,359 @@ try {
|
|
|
| `ImageBuildAPI` | Chapter 4 | 镜像构建(从 Dockerfile 构建、清理中间层) |
|
|
|
| `ContainerManageAPI` | Chapter 5 | 容器管理(创建、启动、停止、删除、列出、查看详情) |
|
|
|
| `ContainerOpsAPI` | Chapter 6 | 容器运维(日志、执行命令、复制文件、等待退出) |
|
|
|
+| `ContainerAdvancedAPI` | Chapter 8 | 容器高级创建(HostConfig:重启策略、资源限制、端口映射) |
|
|
|
+| `VolumeNetworkAPI` | Chapter 9 | 数据卷与网络管理(卷的生命周期、自定义网络、DNS 互联) |
|
|
|
+| `RegistryPushAPI` | Chapter 10 | 私有镜像仓库(本地 registry、认证、推送拉取闭环) |
|
|
|
+| `MaintenanceOpsAPI` | Chapter 11 | 运行监控与日常维护(stats/top/logs -f、diff、commit/export、update) |
|
|
|
+
|
|
|
+***
|
|
|
+
|
|
|
+## 容器高级配置与生产化
|
|
|
+
|
|
|
+前面的章节里,我们创建容器时只用了最基本的参数。真实生产环境中,容器需要设置重启策略、内存/CPU 限制、非 root 用户运行、端口发布——这些都属于 `HostConfig` 的配置范围。本章围绕 `ContainerAdvancedAPI` 演示如何用一个 `createContainerCmd` 一步到位完成 `docker run` 的常用生产级参数。
|
|
|
+
|
|
|
+### HostConfig 核心字段
|
|
|
+
|
|
|
+`HostConfig` 是 docker-java 中承载"容器运行时配置"的核心模型类。它对应 `docker run` 的 `-m`、`--cpus`、`--restart`、`--read-only`、`-p` 等参数,通过 Builder 模式链式调用,最后通过 `withHostConfig(hostConfig)` 传入容器创建命令。
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令:
|
|
|
+// docker run -d --name web-app \
|
|
|
+// --restart=unless-stopped -m 128m --cpus 0.5 \
|
|
|
+// --user 1000:1000 --read-only \
|
|
|
+// -p 8080:80 nginx:latest
|
|
|
+
|
|
|
+HostConfig hostConfig = HostConfig.newHostConfig()
|
|
|
+ .withRestartPolicy(RestartPolicy.unlessStoppedRestart()) // --restart=unless-stopped
|
|
|
+ .withMemory(128L * 1024 * 1024) // -m 128m(字节)
|
|
|
+ .withNanoCPUs(500_000_000L) // --cpus 0.5(纳核)
|
|
|
+ .withReadonlyRootfs(true) // --read-only
|
|
|
+ .withPortBindings(portBindings); // -p 8080:80
|
|
|
+
|
|
|
+dockerClient.createContainerCmd("nginx:latest")
|
|
|
+ .withName("web-app")
|
|
|
+ .withUser("1000:1000") // --user
|
|
|
+ .withExposedPorts(exposedPort)
|
|
|
+ .withHostConfig(hostConfig)
|
|
|
+ .exec();
|
|
|
+```
|
|
|
+
|
|
|
+docker-java 中有两个常见的"单位陷阱"需要注意:
|
|
|
+- `withMemory()` 接受 `long` 类型的**字节数**,而不是 MB——直接传 `128` 会限制在 128 字节,而不是 128MB。
|
|
|
+- `withNanoCPUs()` 接受纳核数(1核 = 10⁹),而 `--cpus 0.5` = 5×10⁸ 纳核。
|
|
|
+
|
|
|
+### 运行结果
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:14.428 ContainerAdvancedAPI -- 生产风格容器 test-prod-xxx 启动成功,访问地址: http://localhost:8080/
|
|
|
+00:11:14.451 ContainerAdvancedAPI -- 容器 xxx 主机配置 -> 重启策略: unless-stopped, 内存限制: 128MB, CPU限制: 0.5核
|
|
|
+00:11:14.898 ContainerAdvancedAPI -- 带资源限制的容器创建成功,ID: xxx, 内存: 256MB, CPU: 1.0核
|
|
|
+```
|
|
|
+
|
|
|
+通过 `inspectContainerCmd` 返回的 `HostConfig` 对象,可以清晰地看到重启策略、内存、CPU 限制均已生效。
|
|
|
+
|
|
|
+### 端口映射:ExposedPort 与 Ports
|
|
|
+
|
|
|
+`docker run -p 8080:80` 的 SDK 对应实现由 `ExposedPort`(声明容器端口)和 `Ports`(绑定宿主机端口)两步完成:
|
|
|
+
|
|
|
+```java
|
|
|
+ExposedPort exposedPort = ExposedPort.tcp(80); // 容器内 80 端口
|
|
|
+Ports portBindings = new Ports();
|
|
|
+portBindings.bind(exposedPort, Ports.Binding.bindPort(8080)); // 绑定到宿主机 8080
|
|
|
+
|
|
|
+HostConfig hostConfig = HostConfig.newHostConfig().withPortBindings(portBindings);
|
|
|
+// ...随后传入 createContainerCmd
|
|
|
+```
|
|
|
+
|
|
|
+> 注意:`ExposedPort` 必须同时传给 `withExposedPorts()` 和 `withPortBindings()`——前者是元数据声明,后者是实际绑定。
|
|
|
+
|
|
|
+**思考:** `withRestartPolicy(RestartPolicy.unlessStoppedRestart())` 与 `withRestartPolicy(RestartPolicy.alwaysRestart())` 有什么区别?生产环境中应该选哪个?
|
|
|
+
|
|
|
+两者都在容器退出后自动重启,区别在于:`always` 在 Docker daemon 重启后也会拉起容器,而 `unless-stopped` 在 daemon 重启后**不会**拉起被手动 `docker stop` 过的容器。生产环境通常选 `unless-stopped`——既能自动恢复崩溃,又尊重运维的手动停止操作。
|
|
|
+
|
|
|
+***
|
|
|
+
|
|
|
+## 数据卷与网络管理
|
|
|
+
|
|
|
+容器是短暂的——一旦删除,内部数据就随之消失。在真实应用中,数据库文件、用户上传等需要持久化存储;多个容器之间也需要相互通信。本章围绕 `VolumeNetworkAPI` 演示 Docker 数据卷(Volume)的创建、挂载、数据持久化,以及自定义 bridge 网络的容器互联与 DNS 发现。
|
|
|
+
|
|
|
+### 数据卷的基本操作
|
|
|
+
|
|
|
+数据卷是 Docker 推荐的持久化存储方式,由 Docker 管理,独立于容器生命周期。
|
|
|
+
|
|
|
+```java
|
|
|
+// 创建数据卷
|
|
|
+// 等价命令: docker volume create my-volume
|
|
|
+dockerClient.createVolumeCmd().withName("my-volume").exec();
|
|
|
+
|
|
|
+// 列出所有数据卷
|
|
|
+// 等价命令: docker volume ls
|
|
|
+List<InspectVolumeResponse> volumes = dockerClient.listVolumesCmd().exec().getVolumes();
|
|
|
+
|
|
|
+// 查看详情(获取宿主机上的实际挂载路径)
|
|
|
+// 等价命令: docker volume inspect my-volume
|
|
|
+InspectVolumeResponse vol = dockerClient.inspectVolumeCmd("my-volume").exec();
|
|
|
+// vol.getMountpoint() -> /var/lib/docker/volumes/my-volume/_data
|
|
|
+```
|
|
|
+
|
|
|
+运行结果:
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:09.162 VolumeNetworkAPI -- 数据卷 test-vol-xxx 创建成功
|
|
|
+00:11:09.182 VolumeNetworkAPI -- 本机共有 41 个数据卷
|
|
|
+00:11:09.186 VolumeNetworkAPI -- 数据卷 test-vol-xxx 挂载点: /var/lib/docker/volumes/test-vol-xxx/_data, 驱动: local
|
|
|
+```
|
|
|
+
|
|
|
+### 挂载卷到容器(Bind)
|
|
|
+
|
|
|
+`docker run -v my-volume:/data` 在 SDK 中通过 `Bind` 对象实现:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker run --name app -v my-volume:/data app:latest
|
|
|
+Bind bind = new Bind("my-volume", new Volume("/data"));
|
|
|
+HostConfig hostConfig = HostConfig.newHostConfig().withBinds(bind);
|
|
|
+
|
|
|
+CreateContainerResponse container = dockerClient.createContainerCmd("amazoncorretto:17")
|
|
|
+ .withName("app")
|
|
|
+ .withEntrypoint("sh", "-c")
|
|
|
+ .withCmd("sleep 300")
|
|
|
+ .withHostConfig(hostConfig)
|
|
|
+ .exec();
|
|
|
+dockerClient.startContainerCmd(container.getId()).exec();
|
|
|
+```
|
|
|
+
|
|
|
+在容器 A 中写入 `/data/hello.txt`,删除容器 A 后启动容器 B 并挂载同一个卷,数据依然存在——这就是数据持久化的核心价值。测试日志印证了这一特性:
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:11.268 VolumeNetworkAPI -- 挂载数据卷的容器创建成功,ID: xxx (容器A)
|
|
|
+00:11:11.482 VolumeNetworkAPITest -- 容器A写入结果: (写入 /data/hello.txt)
|
|
|
+00:11:11.679 VolumeNetworkAPI -- 容器 xxx 已删除 (删除容器A)
|
|
|
+00:11:11.720 VolumeNetworkAPI -- 挂载数据卷的容器创建成功,ID: xxx (容器B,同卷)
|
|
|
+ → 读取到容器A写入的文件
|
|
|
+```
|
|
|
+
|
|
|
+### 自定义网络与容器 DNS
|
|
|
+
|
|
|
+Docker 默认的 `bridge` 网络不支持容器名 DNS 解析;自定义 bridge 网络会自动启用内嵌 DNS,使容器名可直接作为主机名使用。
|
|
|
+
|
|
|
+```java
|
|
|
+// 创建自定义 bridge 网络
|
|
|
+// 等价命令: docker network create my-network
|
|
|
+CreateNetworkResponse response = dockerClient.createNetworkCmd()
|
|
|
+ .withName("my-network")
|
|
|
+ .withDriver("bridge")
|
|
|
+ .exec();
|
|
|
+
|
|
|
+// 在自定义网络中启动容器(容器名自动注册为 DNS)
|
|
|
+// 等价命令: docker run -d --name web-a --network my-network ...
|
|
|
+HostConfig hostConfig = HostConfig.newHostConfig().withNetworkMode("my-network");
|
|
|
+```
|
|
|
+
|
|
|
+验证 DNS:在同一网络中,容器 B 可通过 `getent hosts dns-a` 解析容器 A 的名称:
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:10.463 VolumeNetworkAPITest -- 容器B解析容器A的结果:
|
|
|
+ 172.18.0.2 dns-a-66813360 ← 容器B能通过容器名访问容器A
|
|
|
+```
|
|
|
+
|
|
|
+**思考:** 为什么在默认 bridge 网络下 `ping container-a` 会失败,而在自定义网络下可以成功?
|
|
|
+
|
|
|
+默认 bridge 网络不启用 DNS 解析,只能通过 IP 通信;自定义网络启用 Docker 内嵌 DNS,容器名会自动注册为 A 记录。这是生产环境中几乎总是推荐使用自定义网络的主要原因。
|
|
|
+
|
|
|
+***
|
|
|
+
|
|
|
+## 私有镜像仓库与推送
|
|
|
+
|
|
|
+将镜像推送到 Registry 是 CI/CD 流程的关键环节。本章以本地 `registry:2` 为对象,用 `RegistryPushAPI` 完整演示 `docker login` → `docker tag` → `docker push` → `docker pull` 的闭环过程。
|
|
|
+
|
|
|
+### 启动本地私有仓库
|
|
|
+
|
|
|
+`registry:2` 是 Docker 官方提供的镜像仓库实现,默认监听 5000 端口:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker run -d --name my-registry -p 5000:5000 registry:2
|
|
|
+ExposedPort registryPort = ExposedPort.tcp(5000);
|
|
|
+Ports portBindings = new Ports();
|
|
|
+portBindings.bind(registryPort, Ports.Binding.bindPort(hostPort));
|
|
|
+
|
|
|
+dockerClient.createContainerCmd("registry:2")
|
|
|
+ .withName(containerName)
|
|
|
+ .withExposedPorts(registryPort)
|
|
|
+ .withHostConfig(HostConfig.newHostConfig().withPortBindings(portBindings))
|
|
|
+ .exec();
|
|
|
+```
|
|
|
+
|
|
|
+### 认证、打标签、推送
|
|
|
+
|
|
|
+docker-java 中的 `docker login` 对应 `authCmd()`,`docker push` 对应 `pushImageCmd()`:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker login localhost:5000
|
|
|
+AuthConfig authConfig = new AuthConfig()
|
|
|
+ .withUsername("docker-java-demo")
|
|
|
+ .withPassword("demo-password")
|
|
|
+ .withRegistryAddress("localhost:5000");
|
|
|
+
|
|
|
+AuthResponse response = dockerClient.authCmd().withAuthConfig(authConfig).exec();
|
|
|
+// response.getStatus() -> "Login Succeeded"
|
|
|
+
|
|
|
+// 等价命令:
|
|
|
+// docker tag nginx:latest localhost:5000/myapp:v1
|
|
|
+// docker push localhost:5000/myapp:v1
|
|
|
+dockerClient.tagImageCmd("nginx:latest", "localhost:5000/myapp", "v1").exec();
|
|
|
+
|
|
|
+dockerClient.pushImageCmd("localhost:5000/myapp")
|
|
|
+ .withTag("v1")
|
|
|
+ .exec(new ResultCallback.Adapter<>()) // docker-java 3.7.1 使用通用回调
|
|
|
+ .awaitCompletion(); // 阻塞等待推送完成
|
|
|
+```
|
|
|
+
|
|
|
+推送完成后删除本地副本,再从仓库拉取,验证闭环:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker pull localhost:5000/myapp:v1
|
|
|
+dockerClient.pullImageCmd("localhost:5000/myapp:v1")
|
|
|
+ .exec(new PullImageResultCallback())
|
|
|
+ .awaitCompletion();
|
|
|
+```
|
|
|
+
|
|
|
+### 运行结果
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:08.150 RegistryPushAPI -- 本地私有仓库已启动: http://localhost:41385/
|
|
|
+00:11:08.162 RegistryPushAPI -- Registry localhost:41385 认证结果: Login Succeeded
|
|
|
+00:11:08.489 RegistryPushAPI -- 镜像 localhost:41385/myapp-xxx:v1 推送成功
|
|
|
+00:11:08.769 RegistryPushAPI -- 镜像 localhost:41385/myapp-xxx:v1 从仓库拉取成功
|
|
|
+```
|
|
|
+
|
|
|
+> 注意:docker-java 3.7.1 中 `pushImageCmd()` 没有专用的 `PushImageResultCallback`,需使用 `ResultCallback.Adapter` 通用回调配合 `awaitCompletion()` 等待推送完成。这是版本限制,升级到更高版本后可使用专用回调类。
|
|
|
+
|
|
|
+**思考:** `docker export` 导出的容器文件系统 tar 与 `docker save` 导出的镜像 tar 有什么区别?各自适合什么场景?
|
|
|
+
|
|
|
+`docker export` 导出的是**容器运行后的合并文件系统**,不含镜像层历史和元数据,适用于"把运行环境打包成单层镜像"的场景(配合 `docker import`)。`docker save` 导出的是**完整镜像**(含所有层、配置和标签),适用于镜像迁移和备份。本教程 `MaintenanceOpsAPI` 中演示了 `exportContainer`,`ImageManageAPI` 中演示了 `saveImage`,两者可对比使用。
|
|
|
+
|
|
|
+***
|
|
|
+
|
|
|
+## 运行监控与日常维护
|
|
|
+
|
|
|
+容器上线后,运维人员需要随时查看进程状态、资源占用、日志流、文件系统变更,以及动态调整配置。本章围绕 `MaintenanceOpsAPI` 演示 Docker 运行时监控和日常维护操作,包括 `docker stats --no-stream`、`docker top`、`docker logs -f`、`docker diff`、`docker commit`、`docker export`、`docker cp`(放入方向)和 `docker update`。
|
|
|
+
|
|
|
+### 实时监控:stats 与 top
|
|
|
+
|
|
|
+`docker stats --no-stream` 对应 `statsCmd`,需要通过回调接收 `Statistics` 对象:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker stats --no-stream <containerId>
|
|
|
+AtomicReference<Statistics> snapshot = new AtomicReference<>();
|
|
|
+dockerClient.statsCmd(containerId)
|
|
|
+ .withNoStream(true) // 只取一次,不持续输出
|
|
|
+ .exec(new ResultCallback.Adapter<>() {
|
|
|
+ @Override
|
|
|
+ public void onNext(Statistics stats) {
|
|
|
+ snapshot.set(stats);
|
|
|
+ }
|
|
|
+ }).awaitCompletion();
|
|
|
+
|
|
|
+Statistics stats = snapshot.get();
|
|
|
+long usageMb = stats.getMemoryStats().getUsage() / 1024 / 1024;
|
|
|
+long limitMb = stats.getMemoryStats().getLimit() / 1024 / 1024;
|
|
|
+```
|
|
|
+
|
|
|
+`docker top` 对应 `topContainerCmd`,返回容器内进程列表(类似 `ps aux`):
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker top <containerId>
|
|
|
+TopContainerResponse response = dockerClient.topContainerCmd(containerId).exec();
|
|
|
+// response.getTitles() -> ["UID","PID","PPID","C","STIME","TTY","TIME","CMD"]
|
|
|
+// response.getProcesses() -> [["root","3749932","3749909","12","00:11","?","00:00:00","sleep 300"]]
|
|
|
+```
|
|
|
+
|
|
|
+### 实时日志跟随(logs -f)
|
|
|
+
|
|
|
+Chapter 6 中的 `getContainerLogs` 是一次性读取;这里演示的是**持续跟随模式**,对应 `docker logs -f`:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker logs -f --tail 50 <containerId>
|
|
|
+LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
|
|
|
+ .withStdOut(true)
|
|
|
+ .withStdErr(true)
|
|
|
+ .withFollowStream(true) // 关键:进入跟随模式
|
|
|
+ .withTail(50);
|
|
|
+
|
|
|
+ResultCallback<Frame> callback = cmd.exec(new ResultCallback.Adapter<>() {
|
|
|
+ @Override
|
|
|
+ public void onNext(Frame frame) {
|
|
|
+ output.append(new String(frame.getPayload()));
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 跟随指定时长后主动关闭(模拟 Ctrl+C)
|
|
|
+Thread.sleep(followSeconds * 1000);
|
|
|
+callback.close();
|
|
|
+```
|
|
|
+
|
|
|
+### 文件系统变更查看(docker diff)
|
|
|
+
|
|
|
+`docker diff` 列出容器相对原始镜像的新增、修改和删除文件:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker diff <containerId>
|
|
|
+List<ChangeLog> changes = dockerClient.containerDiffCmd(containerId).exec();
|
|
|
+// kind: 0=已修改 1=新增 2=删除
|
|
|
+```
|
|
|
+
|
|
|
+测试在容器内创建一个新文件后,diff 输出:
|
|
|
+
|
|
|
+```log
|
|
|
+00:11:15.555 MaintenanceOpsAPI -- 修改 /tmp
|
|
|
+00:11:15.555 MaintenanceOpsAPI -- 新增 /tmp/diff-test.txt
|
|
|
+```
|
|
|
+
|
|
|
+### 归档操作:commit 与 export
|
|
|
+
|
|
|
+`docker commit` 将容器当前状态保存为新镜像,保留镜像层历史:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker commit <containerId> my-app:v3
|
|
|
+String imageId = dockerClient.commitCmd(containerId)
|
|
|
+ .withRepository("my-app")
|
|
|
+ .withTag("v3")
|
|
|
+ .withMessage("committed by docker-java")
|
|
|
+ .exec();
|
|
|
+```
|
|
|
+
|
|
|
+`docker export` 将容器文件系统导出为 tar(不含层历史,只保留当前快照):
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker export <containerId> -o backup.tar
|
|
|
+try (InputStream tarStream = dockerClient.exportContainerCmd(containerId).exec();
|
|
|
+ FileOutputStream fos = new FileOutputStream("backup.tar")) {
|
|
|
+ tarStream.transferTo(fos);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **重要提示:** `exportContainerCmd` 对**运行中**容器导出可能产生不完整的文件系统快照。建议在导出前先 `stopContainer`,以避免 containerd 解压时出现 `unexpected EOF`。`docker import` 在 containerd snapshotter 下对某些基础镜像的导出 tar 解包不稳定,实际生产中更推荐 `docker save`/`docker load`。
|
|
|
+
|
|
|
+### 动态更新容器(docker update)
|
|
|
+
|
|
|
+`docker update` 允许在不重建容器的情况下调整资源限制:
|
|
|
+
|
|
|
+```java
|
|
|
+// 等价命令: docker update --memory 256m <containerId>
|
|
|
+// 注意:Docker 要求 memory-swap >= memory,必须同时设置
|
|
|
+long memoryBytes = 256 * 1024 * 1024;
|
|
|
+dockerClient.updateContainerCmd(containerId)
|
|
|
+ .withMemory(memoryBytes)
|
|
|
+ .withMemorySwap(memoryBytes * 2) // 必须同时设置,否则返回 409
|
|
|
+ .exec();
|
|
|
+```
|
|
|
+
|
|
|
+**思考:** `docker commit` 和 `docker build` 都能产生镜像,为什么生产环境中总是推荐使用 `docker build`?
|
|
|
+
|
|
|
+`docker commit` 把容器的完整文件系统合并成一个不透明的镜像层,无法追溯修改历史、无法复现构建过程、镜像体积无法优化。`docker build` 通过 Dockerfile 定义构建步骤,每条指令产生一个可缓存的层,支持版本控制、增量构建和安全审查。`docker commit` 适用于紧急现场保存,而 `docker build` 是唯一推荐的生产构建方式。
|
|
|
+
|
|
|
+***
|
|
|
|
|
|
> Docker Java SDK 的 API 设计遵循"命令对象模式":每个操作对应一个 `*Cmd` 接口,通过 Builder 模式链式设置参数,最后通过 `.exec()` 统一执行。掌握这个模式后,即使遇到本教程未覆盖的 API,也能快速上手。
|
|
|
+
|
|
|
+
|