Procházet zdrojové kódy

docs: add Chapter 8-11 tutorial, fix pingCmd and add prune to cheat sheet

yangyi před 2 dny
rodič
revize
cd943eff32
1 změnil soubory, kde provedl 363 přidání a 2 odebrání
  1. 363 2
      docker-java-sdk-tutorial.md

+ 363 - 2
docker-java-sdk-tutorial.md

@@ -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,也能快速上手。
+
+