Explorar o código

feat: add Chapter 8 advanced container creation (HostConfig) and Chapter 9 volume/network management

yangyi hai 2 días
pai
achega
cdcd1f2342

+ 39 - 0
docker-java-sdk-tutorial.md

@@ -213,10 +213,12 @@ public class QuickStart {
         DockerClient dockerClient = DockerClientBuilder.getInstance().build();
 
         // 使用 ping 命令测试连接是否成功
+        // 等价命令: docker version(连接自检,底层请求 HTTP GET /_ping)
         dockerClient.pingCmd().exec();
         log.info("Docker client ping 成功!");
 
         // 列出所有本地镜像,验证连接正常
+        // 等价命令: docker images
         dockerClient.listImagesCmd().exec().forEach(image -> {
             log.info("Docker 镜像信息: {}", image);
         });
@@ -293,10 +295,12 @@ public class QuickStart {
         DockerClient dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient);
 
         // 测试连接
+        // 等价命令: docker version(连接自检)
         dockerClient.pingCmd().exec();
         log.info("自定义配置连接成功!");
 
         // 列出所有镜像
+        // 等价命令: docker images
         dockerClient.listImagesCmd().exec().forEach(image -> {
             log.info("Docker 镜像信息: {}", image);
         });
@@ -315,6 +319,7 @@ public class QuickStart {
 
 ```java
 try {
+    // 等价命令: docker version(连接自检)
     dockerClient.pingCmd().exec();
     log.info("Docker 连接成功!");
 } catch (Exception e) {
@@ -326,10 +331,12 @@ try {
 
 ```java
 // 获取 Docker 版本信息
+// 等价命令: docker version
 String version = dockerClient.versionCmd().exec().getVersion();
 log.info("Docker 版本: {}", version);
 
 // 获取 Docker 系统信息
+// 等价命令: docker info
 Info info = dockerClient.infoCmd().exec();
 log.info("Docker 系统信息: {}", info);
 ```
@@ -350,6 +357,7 @@ import com.github.dockerjava.api.model.PullResponseItem;
 public void pullImage(String imageName) {
     try {
         // 构建并执行拉取命令
+        // 等价命令: docker pull nginx:latest
         // exec() 方法会阻塞直到镜像拉取完成
         dockerClient.pullImageCmd(imageName)
                 .exec(new PullImageResultCallback())
@@ -382,6 +390,7 @@ import com.github.dockerjava.api.model.Ports;
 public void createAndStartContainer(String imageName, String containerName) {
     // 步骤1:创建容器
     // 创建容器只是分配资源和配置,并不会真正运行
+    // 等价命令: docker create --name demo-container hello-world
     CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
             .withName(containerName)  // 设置容器名称
             .exec();
@@ -389,6 +398,8 @@ public void createAndStartContainer(String imageName, String containerName) {
     log.info("容器创建成功,ID: {}", container.getId());
 
     // 步骤2:启动容器
+    // 等价命令: docker start demo-container
+    // 上面两步合起来相当于: docker run --name demo-container hello-world
     dockerClient.startContainerCmd(container.getId()).exec();
     log.info("容器 {} 启动成功!", containerName);
 }
@@ -449,11 +460,13 @@ public class QuickStart {
      */
     public void runDemo() {
         // 1. 测试连接
+        // 等价命令: docker version
         dockerClient.pingCmd().exec();
         log.info("1. Docker 连接成功");
 
         // 2. 拉取 hello-world 镜像
         try {
+            // 等价命令: docker pull nginx:latest
             dockerClient.pullImageCmd("hello-world")
                     .exec(new com.github.dockerjava.api.command.PullImageResultCallback())
                     .awaitCompletion();
@@ -465,11 +478,14 @@ public class QuickStart {
         }
 
         // 3. 创建并启动容器
+        // 等价命令: docker create --name demo-container hello-world
         CreateContainerResponse container = dockerClient.createContainerCmd("hello-world")
                 .withName("demo-container")
                 .exec();
         log.info("3. 容器创建成功,ID: {}", container.getId());
 
+        // 等价命令: docker start demo-container
+        // 上面两步合起来相当于: docker run --name demo-container hello-world
         dockerClient.startContainerCmd(container.getId()).exec();
         log.info("4. 容器启动成功");
     }
@@ -502,6 +518,7 @@ public class QuickStart {
  * @return 镜像列表
  */
 public List<Image> listImages() {
+    // 等价命令: docker images -a(列出所有镜像,包括中间层缩影)
     List<Image> images = dockerClient.listImagesCmd()
             .withShowAll(true)      // 展示所有镜像(包括中间层)
             .exec();
@@ -538,6 +555,7 @@ Docker 的镜像采用分层存储架构,一个镜像由多个 Layer 叠加而
  * @return 镜像详情
  */
 public InspectImageResponse inspectImage(String imageId) {
+    // 等价命令: docker inspect <imageId>
     InspectImageResponse response = dockerClient.inspectImageCmd(imageId).exec();
     log.info("镜像 {} 详情: {}", imageId, response);
     return response;
@@ -572,6 +590,7 @@ public InspectImageResponse inspectImage(String imageId) {
  */
 public boolean pullImage(String imageName) {
     try {
+        // 等价命令: docker pull ubuntu:latest
         // exec() 执行命令,返回异步回调
         // awaitCompletion() 阻塞等待镜像拉取完成
         dockerClient.pullImageCmd(imageName)
@@ -604,6 +623,8 @@ public boolean pullImage(String imageName) {
  * @param force 是否强制删除(即使镜像被容器使用)
  */
 public void removeImage(String imageId, boolean force) {
+    // 等价命令: docker rmi <imageId>
+    // 等价命令: docker rmi -f <imageId>(force=true 时强制删除)
     dockerClient.removeImageCmd(imageId)
             .withForce(force)   // 强制删除
             .exec();
@@ -625,6 +646,7 @@ public void removeImage(String imageId, boolean force) {
  * @param tag 新的标签名
  */
 public void tagImage(String imageId, String repository, String tag) {
+    // 等价命令: docker tag <imageId> <repository>:<tag>
     dockerClient.tagImageCmd(imageId, repository, tag).exec();
     log.info("镜像 {} 已标记为 {}:{}", imageId, repository, tag);
 }
@@ -644,6 +666,7 @@ Docker 还支持将镜像导出为 tar 文件,或者从 tar 文件导入镜像
 // 将镜像导出为 tar 文件
 public void saveImage(String imageId, String filePath) {
     try (OutputStream outputStream = new FileOutputStream(filePath)) {
+        // 等价命令: docker save <imageId> -o nginx-backup.tar
         dockerClient.saveImageCmd(imageId).exec(outputStream);
         log.info("镜像 {} 已导出到 {}", imageId, filePath);
     } catch (IOException e) {
@@ -654,6 +677,7 @@ public void saveImage(String imageId, String filePath) {
 // 从 tar 文件加载镜像
 public void loadImage(String filePath) {
     try (InputStream inputStream = new FileInputStream(filePath)) {
+        // 等价命令: docker load -i nginx-backup.tar
         dockerClient.loadImageCmd(inputStream)
                 .exec(new LoadImageResultCallback())
                 .awaitCompletion();
@@ -718,6 +742,7 @@ public String buildImage(String dockerfilePath, String imageName) {
     }
 
     try {
+        // 等价命令: docker build -f Dockerfile -t sdk-demo/hello:latest .
         // withDockerfile 指定 Dockerfile 文件位置
         // withBaseDirectory 设置构建上下文(当前目录)
         // withTag 指定构建后的镜像名称和标签
@@ -846,6 +871,7 @@ Docker 的设计哲学是:**创建**和**启动**是分离的操作。创建
  */
 public CreateContainerResponse createContainer(String imageName, String containerName) {
     // 创建容器:分配资源配置,但不会真正运行
+    // 等价命令: docker create --name my-container nginx:latest
     // withName 指定容器名称(可选,不指定时 Docker 自动生成)
     CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
             .withName(containerName)
@@ -871,12 +897,14 @@ public CreateContainerResponse createContainer(String imageName, String containe
 
 ```java
 // 创建带环境变量的容器
+// 等价命令: docker create --name my-container -e MYSQL_ROOT_PASSWORD=123456 nginx:latest
 dockerClient.createContainerCmd("nginx:latest")
         .withName("my-nginx")
         .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80")
         .exec();
 
 // 创建带端口映射的容器
+// 等价命令: docker create --name my-container -p 8080:80 nginx:latest
 dockerClient.createContainerCmd("nginx:latest")
         .withName("my-nginx")
         .withExposedPorts(ExposedPort.tcp(80))
@@ -894,6 +922,7 @@ dockerClient.createContainerCmd("nginx:latest")
  * @param containerId 容器 ID
  */
 public void startContainer(String containerId) {
+    // 等价命令: docker start <containerId>
     dockerClient.startContainerCmd(containerId).exec();
     log.info("容器 {} 启动成功", containerId);
 }
@@ -914,6 +943,7 @@ public void startContainer(String containerId) {
  * @param timeoutSeconds 等待时长(秒),超过则强制终止
  */
 public void stopContainer(String containerId, Integer timeoutSeconds) {
+    // 等价命令: docker stop -t 10 <containerId>(先发 SIGTERM,超时后 SIGKILL)
     dockerClient.stopContainerCmd(containerId)
             .withTimeout(timeoutSeconds)  // SIGTERM 后等待时间
             .exec();
@@ -931,6 +961,7 @@ public void stopContainer(String containerId, Integer timeoutSeconds) {
  * @param containerId 容器 ID
  */
 public void killContainer(String containerId) {
+    // 等价命令: docker kill <containerId>(立即发送 SIGKILL,不给优雅退出机会)
     dockerClient.killContainerCmd(containerId).exec();
     log.info("容器 {} 已被强制停止", containerId);
 }
@@ -952,6 +983,8 @@ public void killContainer(String containerId) {
  * @param removeVolumes 是否同时删除关联的卷
  */
 public void removeContainer(String containerId, boolean force, boolean removeVolumes) {
+    // 等价命令: docker rm <containerId>
+    // 等价命令: docker rm -f -v <containerId>(同时强制停止并删除数据卷)
     dockerClient.removeContainerCmd(containerId)
             .withForce(force)            // 强制删除运行中的容器
             .withRemoveVolumes(removeVolumes)  // 删除关联数据卷
@@ -970,6 +1003,7 @@ public void removeContainer(String containerId, boolean force, boolean removeVol
  * @return 容器列表
  */
 public List<Container> listContainers() {
+    // 等价命令: docker ps -a(列出所有容器,包括已停止的)
     // withShowAll(true) 展示所有容器(不只运行中的)
     List<Container> containers = dockerClient.listContainersCmd()
             .withShowAll(true)
@@ -1002,6 +1036,7 @@ public List<Container> listContainers() {
  * @return 容器详情
  */
 public InspectContainerResponse inspectContainer(String containerId) {
+    // 等价命令: docker inspect <containerId>(查看容器完整 JSON 详情)
     InspectContainerResponse response = dockerClient.inspectContainerCmd(containerId).exec();
     log.info("容器 {} 状态: {}, 名称: {}",
             containerId, response.getState().getStatus(), response.getName());
@@ -1068,6 +1103,7 @@ Docker 的设计中,一个容器可以有多个名称(别名),通常是
 public String getContainerLogs(String containerId, int tailLines) {
     StringBuilder logBuilder = new StringBuilder();
     try {
+        // 等价命令: docker logs --tail 10 <containerId>
         // withTail 只获取末尾 N 行
         LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
                 .withStdOut(true)     // 获取标准输出
@@ -1126,6 +1162,7 @@ Docker 容器的日志实际上包含两个数据流:标准输出(STDOUT)
 public String execCommandInContainer(String containerId, String... command) {
     StringBuilder output = new StringBuilder();
     try {
+        // 等价命令: docker exec <containerId> ls -la /
         // 1. 创建 exec 实例(描述要在容器中执行什么命令)
         ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                 .withCmd(command)              // 要执行的命令
@@ -1170,6 +1207,7 @@ public String execCommandInContainer(String containerId, String... command) {
  */
 public int waitContainer(String containerId, int timeoutSeconds) {
     try {
+        // 等价命令: docker wait <containerId>(阻塞直到容器退出,返回退出码)
         WaitContainerResultCallback callback = dockerClient.waitContainerCmd(containerId)
                 .exec(new WaitContainerResultCallback());
         // 阻塞等待容器退出,设置超时
@@ -1207,6 +1245,7 @@ public void opsDemo() {
         log.info("3. 容器日志:\n{}", logs);
 
         // 4. 清理容器
+        // 等价命令: docker rm -f <containerId>(测试清理用)
         dockerClient.removeContainerCmd(id).withForce(true).exec();
         log.info("4. 临时容器已清理");
     } catch (Exception e) {

+ 193 - 0
src/main/java/space/anyi/docker/ContainerAdvancedAPI.java

@@ -0,0 +1,193 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.HostConfig;
+import com.github.dockerjava.api.model.Ports;
+import com.github.dockerjava.api.model.RestartPolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Docker 容器高级创建 API 示例
+ * 演示 docker run 的完整生产级参数:重启策略、资源限制、用户、工作目录、只读根文件系统等
+ * 每个操作旁标注了等价的 Docker CLI 命令,方便对照理解
+ */
+public class ContainerAdvancedAPI {
+    private static final Logger log = LoggerFactory.getLogger(ContainerAdvancedAPI.class);
+
+    private final DockerClient dockerClient;
+
+    public ContainerAdvancedAPI() {
+        this.dockerClient = DockerClientFactory.createDockerClient();
+    }
+
+    /**
+     * 创建带重启策略的容器
+     * 生产环境中容器崩溃后需要自动拉起,通过 RestartPolicy 实现
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param restartPolicy 重启策略(always / unless-stopped / on-failure:N)
+     * @return 容器响应
+     */
+    public CreateContainerResponse createContainerWithRestartPolicy(String imageName, String containerName,
+                                                                    RestartPolicy restartPolicy) {
+        // 等价命令: docker create --name my-app --restart=unless-stopped app:latest
+        //           --restart 可选值:no / always / unless-stopped / on-failure[:N]
+        // 重启策略决定 Docker daemon 或宿主机重启后,容器是否自动拉起
+        HostConfig hostConfig = HostConfig.newHostConfig()
+                .withRestartPolicy(restartPolicy);
+
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withHostConfig(hostConfig)
+                .exec();
+        log.info("带重启策略({})的容器创建成功,ID: {}", restartPolicy.getName(), response.getId());
+        return response;
+    }
+
+    /**
+     * 创建带资源限制的容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param memoryMb 内存限制(MB)
+     * @param cpuCount CPU 核数限制
+     * @return 容器响应
+     */
+    public CreateContainerResponse createContainerWithResourceLimit(String imageName, String containerName,
+                                                                    long memoryMb, double cpuCount) {
+        // 等价命令: docker run -d --name my-app -m 256m --cpus 1 app:latest
+        //           -m 256m 限制最大内存;--cpus 1 限制 CPU 配额
+        // docker-java 中内存单位是字节(Byte),CPU 单位是纳核(nano CPU,1 核 = 10^9)
+        HostConfig hostConfig = HostConfig.newHostConfig()
+                .withMemory(memoryMb * 1024 * 1024)          // MB -> Byte
+                .withNanoCPUs((long) (cpuCount * 1_000_000_000L)); // 核 -> 纳核
+
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withHostConfig(hostConfig)
+                .exec();
+        log.info("带资源限制的容器创建成功,ID: {}, 内存: {}MB, CPU: {}核", response.getId(), memoryMb, cpuCount);
+        return response;
+    }
+
+    /**
+     * 创建指定用户、工作目录的容器
+     * 安全最佳实践:生产环境用非 root 用户运行容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param user 容器内运行用户,如 "nobody" 或 "1000:1000"
+     * @param workingDir 容器内工作目录
+     * @param readOnlyRootfs 根文件系统是否只读(挂载 volume 后写入卷)
+     * @return 容器响应
+     */
+    public CreateContainerResponse createContainerWithUser(String imageName, String containerName,
+                                                           String user, String workingDir, boolean readOnlyRootfs) {
+        // 等价命令: docker run --user nobody -w /app --read-only app:latest
+        //           --user 指定 UID/用户名,--read-only 把根文件系统设为只读(防篡改)
+        HostConfig hostConfig = HostConfig.newHostConfig()
+                .withReadonlyRootfs(readOnlyRootfs);
+
+        CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withHostConfig(hostConfig)
+                .withUser(user)
+                .withWorkingDir(workingDir)
+                .exec();
+        log.info("带用户({})/工作目录({})的容器创建成功,ID: {}", user, workingDir, response.getId());
+        return response;
+    }
+
+    /**
+     * 创建带端口映射的 nginx 容器并启动,验证外部可访问
+     * @param containerName 容器名称
+     * @param hostPort 宿主机端口
+     * @return 容器 ID
+     */
+    public String createAndStartNginx(String containerName, int hostPort) {
+        // 等价命令: docker run -d --name my-nginx -p 8080:80 nginx:latest
+        //           -p 8080:80 暴露 Web 服务,相当于把容器 80 端口发布到宿主机 8080
+        ExposedPort exposedPort = ExposedPort.tcp(80);
+        Ports portBindings = new Ports();
+        portBindings.bind(exposedPort, Ports.Binding.bindPort(hostPort));
+
+        HostConfig hostConfig = HostConfig.newHostConfig().withPortBindings(portBindings);
+
+        CreateContainerResponse container = dockerClient.createContainerCmd("nginx:latest")
+                .withName(containerName)
+                .withExposedPorts(exposedPort)
+                .withHostConfig(hostConfig)
+                .exec();
+
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("nginx 容器 {} 已启动,访问地址: http://localhost:{}/", containerName, hostPort);
+        return container.getId();
+    }
+
+    /**
+     * 综合演示:用一条 createContainerCmd 完成 docker run 的全部常用参数
+     * 重启策略 + 资源限制 + 用户 + 端口映射 + 只读根文件系统
+     * @param containerName 容器名称
+     * @return 容器 ID
+     */
+    public String createProductionStyleContainer(String containerName) {
+        // 等价命令:
+        //   docker run -d --name web-app \
+        //     --restart=unless-stopped -m 128m --cpus 0.5 \
+        //     --user 1000:1000 --read-only \
+        //     -p 8080:80 nginx:latest
+        ExposedPort exposedPort = ExposedPort.tcp(80);
+        Ports portBindings = new Ports();
+        portBindings.bind(exposedPort, Ports.Binding.bindPort(8080));
+
+        HostConfig hostConfig = HostConfig.newHostConfig()
+                .withRestartPolicy(RestartPolicy.unlessStoppedRestart())
+                .withMemory(128L * 1024 * 1024)
+                .withNanoCPUs(500_000_000L)
+                .withReadonlyRootfs(true)
+                .withPortBindings(portBindings);
+
+        CreateContainerResponse container = dockerClient.createContainerCmd("nginx:latest")
+                .withName(containerName)
+                .withUser("1000:1000")
+                .withExposedPorts(exposedPort)
+                .withHostConfig(hostConfig)
+                .exec();
+
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("生产风格容器 {} 启动成功,访问地址: http://localhost:8080/", containerName);
+        return container.getId();
+    }
+
+    /**
+     * 查看容器的高级配置(重启策略、资源限制等)
+     * 等价命令: docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' <containerId>
+     * @param containerId 容器 ID
+     * @return 容器详情
+     */
+    public InspectContainerResponse inspectContainer(String containerId) {
+        InspectContainerResponse response = dockerClient.inspectContainerCmd(containerId).exec();
+        HostConfig hostConfig = response.getHostConfig();
+        log.info("容器 {} 主机配置 -> 重启策略: {}, 内存限制: {}MB, CPU限制: {}核",
+                containerId,
+                hostConfig.getRestartPolicy() != null ? hostConfig.getRestartPolicy().getName() : "none",
+                hostConfig.getMemory() != null ? hostConfig.getMemory() / 1024 / 1024 : 0,
+                hostConfig.getNanoCPUs() != null ? hostConfig.getNanoCPUs() / 1_000_000_000.0 : 0);
+        return response;
+    }
+
+    /**
+     * 删除容器
+     * 等价命令: docker rm -f <containerId>
+     * @param containerId 容器 ID
+     */
+    public void removeContainer(String containerId) {
+        dockerClient.removeContainerCmd(containerId)
+                .withForce(true)
+                .withRemoveVolumes(true)
+                .exec();
+        log.info("容器 {} 已删除", containerId);
+    }
+}

+ 243 - 0
src/main/java/space/anyi/docker/VolumeNetworkAPI.java

@@ -0,0 +1,243 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.CreateNetworkResponse;
+import com.github.dockerjava.api.command.CreateVolumeResponse;
+import com.github.dockerjava.api.command.ExecCreateCmdResponse;
+import com.github.dockerjava.api.command.InspectVolumeResponse;
+import com.github.dockerjava.api.model.Bind;
+import com.github.dockerjava.api.model.HostConfig;
+import com.github.dockerjava.api.model.Network;
+import com.github.dockerjava.api.model.Volume;
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.api.model.StreamType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Docker 数据卷(Volume)与网络(Network)管理 API 示例
+ * 演示数据卷的创建/查询/删除与数据持久化,以及自定义网络的容器互联
+ * 每个操作旁标注了等价的 Docker CLI 命令,方便对照理解
+ */
+public class VolumeNetworkAPI {
+    private static final Logger log = LoggerFactory.getLogger(VolumeNetworkAPI.class);
+
+    private final DockerClient dockerClient;
+
+    public VolumeNetworkAPI() {
+        this.dockerClient = DockerClientFactory.createDockerClient();
+    }
+
+    // ==================== 数据卷管理 ====================
+
+    /**
+     * 创建数据卷
+     * 等价命令: docker volume create my-volume
+     * @param volumeName 卷名称
+     * @return 卷名称
+     */
+    public String createVolume(String volumeName) {
+        CreateVolumeResponse response = dockerClient.createVolumeCmd()
+                .withName(volumeName)
+                .exec();
+        log.info("数据卷 {} 创建成功", response.getName());
+        return response.getName();
+    }
+
+    /**
+     * 列出所有数据卷
+     * 等价命令: docker volume ls
+     * @return 数据卷列表
+     */
+    public List<InspectVolumeResponse> listVolumes() {
+        // 等价命令: docker volume ls
+        List<InspectVolumeResponse> volumes = dockerClient.listVolumesCmd().exec().getVolumes();
+        log.info("本机共有 {} 个数据卷", volumes.size());
+        return volumes;
+    }
+
+    /**
+     * 查看数据卷详情(挂载点、驱动等)
+     * 等价命令: docker volume inspect my-volume
+     * @param volumeName 卷名称
+     * @return 卷详情
+     */
+    public InspectVolumeResponse inspectVolume(String volumeName) {
+        InspectVolumeResponse volume = dockerClient.inspectVolumeCmd(volumeName).exec();
+        log.info("数据卷 {} 挂载点: {}, 驱动: {}", volumeName, volume.getMountpoint(), volume.getDriver());
+        return volume;
+    }
+
+    /**
+     * 删除数据卷
+     * 等价命令: docker volume rm my-volume
+     * @param volumeName 卷名称
+     */
+    public void removeVolume(String volumeName) {
+        dockerClient.removeVolumeCmd(volumeName).exec();
+        log.info("数据卷 {} 已删除", volumeName);
+    }
+
+    /**
+     * 创建挂载数据卷的容器(数据写入卷中)
+     * 等价命令:
+     *   docker create -v my-volume:/data --entrypoint sh corretto:17
+     *   docker start <container>
+     * @param volumeName 卷名称(作为 Bind 的源)
+     * @param containerName 容器名称
+     * @return 容器 ID
+     */
+    public CreateContainerResponse createContainerWithVolume(String volumeName, String containerName) {
+        // 等价命令: docker run --name app -v my-volume:/data app:latest
+        // Bind(卷名称, 容器内路径) 相当于 -v my-volume:/data
+        Bind bind = new Bind(volumeName, new Volume("/data"));
+        HostConfig hostConfig = HostConfig.newHostConfig().withBinds(bind);
+
+        // corretto 镜像自带的 ENTRYPOINT(/usr/local/bin/startup.sh) 会长期挂起,
+        // 这里覆盖为 sleep 保持容器存活,便于后续 exec 读写 /data
+        CreateContainerResponse container = dockerClient.createContainerCmd("amazoncorretto:17")
+                .withName(containerName)
+                .withEntrypoint("sh", "-c")
+                .withCmd("sleep 300")
+                .withHostConfig(hostConfig)
+                .exec();
+        log.info("挂载数据卷的容器创建成功,ID: {}", container.getId());
+        dockerClient.startContainerCmd(container.getId()).exec();
+        return container;
+    }
+
+    /**
+     * 在挂载了数据卷的容器中执行一条 shell 命令
+     * 等价命令: docker exec <containerId> sh -c "echo hello > /data/file.txt"
+     * @param containerId 容器 ID
+     * @param shellCommand 要执行的命令(由 sh -c 包装)
+     * @return 命令输出
+     */
+    public String execShellInContainer(String containerId, String shellCommand) {
+        StringBuilder output = new StringBuilder();
+        try {
+            // 等价命令: docker exec <containerId> sh -c "<shellCommand>"
+            ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
+                    .withCmd("sh", "-c", shellCommand)
+                    .withAttachStdout(true)
+                    .withAttachStderr(true)
+                    .exec();
+            dockerClient.execStartCmd(exec.getId())
+                    .exec(new ResultCallback.Adapter<>() {
+                        @Override
+                        public void onNext(Frame frame) {
+                            if (frame.getStreamType() == StreamType.STDOUT ||
+                                    frame.getStreamType() == StreamType.STDERR) {
+                                output.append(new String(frame.getPayload()));
+                            }
+                        }
+                    }).awaitCompletion();
+            return output.toString();
+        } catch (InterruptedException e) {
+            log.error("容器内执行命令被中断: {}", e.getMessage());
+            Thread.currentThread().interrupt();
+            return output.toString();
+        }
+    }
+
+    // ==================== 网络管理 ====================
+
+    /**
+     * 创建自定义 bridge 网络
+     * 等价命令: docker network create my-network
+     * @param networkName 网络名称
+     * @return 网络 ID
+     */
+    public String createNetwork(String networkName) {
+        // 等价命令: docker network create my-network
+        CreateNetworkResponse response = dockerClient.createNetworkCmd()
+                .withName(networkName)
+                .withDriver("bridge")
+                .exec();
+        log.info("网络 {} 创建成功,ID: {}", networkName, response.getId());
+        return response.getId();
+    }
+
+    /**
+     * 列出所有网络
+     * 等价命令: docker network ls
+     * @return 网络列表
+     */
+    public List<Network> listNetworks() {
+        List<Network> networks = dockerClient.listNetworksCmd().exec();
+        log.info("本机共有 {} 个网络", networks.size());
+        return networks;
+    }
+
+    /**
+     * 查看网络详情
+     * 等价命令: docker network inspect <networkId>
+     * @param networkId 网络 ID 或名称
+     */
+    public void inspectNetwork(String networkId) {
+        dockerClient.inspectNetworkCmd().withNetworkId(networkId).exec();
+        log.info("网络 {} 已查询", networkId);
+    }
+
+    /**
+     * 删除网络
+     * 等价命令: docker network rm my-network
+     * @param networkId 网络 ID 或名称
+     */
+    public void removeNetwork(String networkId) {
+        dockerClient.removeNetworkCmd(networkId).exec();
+        log.info("网络 {} 已删除", networkId);
+    }
+
+    /**
+     * 创建指定网络模式下的容器并启动
+     * 等价命令:
+     *   docker run -d --name web-a --network my-network --entrypoint sh corretto:17 ...
+     * @param imageName 镜像名称
+     * @param containerName 容器名称(同网络内视为 DNS 主机名)
+     * @param networkName 网络名称
+     * @return 容器 ID
+     */
+    public String startContainerOnNetwork(String imageName, String containerName, String networkName) {
+        // 等价命令: docker run -d --name web-a --network my-network ...
+        // 自定义网络会启用 Docker 内嵌 DNS,容器名可直接作为主机名互相访问
+        HostConfig hostConfig = HostConfig.newHostConfig().withNetworkMode(networkName);
+
+        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withEntrypoint("sh", "-c")
+                .withCmd("sleep 300")
+                .withHostConfig(hostConfig)
+                .exec();
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("容器 {} 已加入网络 {}, ID: {}", containerName, networkName, container.getId());
+        return container.getId();
+    }
+
+    /**
+     * 将已启动的容器接入网络(等价 docker network connect)
+     * 等价命令: docker network connect my-network <containerId>
+     * @param networkName 网络名称
+     * @param containerId 容器 ID
+     */
+    public void connectToNetwork(String networkName, String containerId) {
+        dockerClient.connectToNetworkCmd()
+                .withNetworkId(networkName)
+                .withContainerId(containerId)
+                .exec();
+        log.info("容器 {} 已接入网络 {}", containerId, networkName);
+    }
+
+    /**
+     * 删除指定的容器(测试清理用)
+     * @param containerId 容器 ID
+     */
+    public void removeContainer(String containerId) {
+        dockerClient.removeContainerCmd(containerId).withForce(true).exec();
+        log.info("容器 {} 已删除", containerId);
+    }
+}

+ 153 - 0
src/test/java/space/anyi/docker/ContainerAdvancedAPITest.java

@@ -0,0 +1,153 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.RestartPolicy;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 容器高级创建 API 的测试类
+ * 注意:需要本机 Docker daemon 运行中
+ * 使用 nginx:latest 和 amazoncorretto:17 镜像,每个测试自行清理创建的容器
+ */
+class ContainerAdvancedAPITest {
+
+    private String uniqueName(String prefix) {
+        return prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
+    }
+
+    /**
+     * 测试带重启策略创建容器(--restart=unless-stopped)
+     */
+    @Test
+    void createContainerWithRestartPolicy() {
+        ContainerAdvancedAPI api = new ContainerAdvancedAPI();
+        String name = uniqueName("test-restart");
+        CreateContainerResponse container = api.createContainerWithRestartPolicy(
+                "nginx:latest", name, RestartPolicy.unlessStoppedRestart());
+        assertNotNull(container.getId());
+
+        // 验证重启策略已生效
+        InspectContainerResponse inspect = api.inspectContainer(container.getId());
+        assertEquals("unless-stopped", inspect.getHostConfig().getRestartPolicy().getName(),
+                "重启策略应为 unless-stopped");
+
+        api.removeContainer(container.getId());
+    }
+
+    /**
+     * 测试带资源限制创建容器(-m 256m --cpus 1)
+     */
+    @Test
+    void createContainerWithResourceLimit() {
+        ContainerAdvancedAPI api = new ContainerAdvancedAPI();
+        String name = uniqueName("test-resource");
+        CreateContainerResponse container = api.createContainerWithResourceLimit(
+                "nginx:latest", name, 256, 1);
+        assertNotNull(container.getId());
+
+        // 验证资源限制已生效(256MB = 268435456 字节,1 核 = 1000000000 纳核)
+        InspectContainerResponse inspect = api.inspectContainer(container.getId());
+        assertEquals(268435456L, inspect.getHostConfig().getMemory(), "内存限制应为 256MB");
+        assertEquals(1_000_000_000L, inspect.getHostConfig().getNanoCPUs(), "CPU 限制应为 1 核");
+
+        api.removeContainer(container.getId());
+    }
+
+    /**
+     * 测试指定用户和工作目录创建容器(--user nobody --read-only)
+     */
+    @Test
+    void createContainerWithUser() {
+        ContainerAdvancedAPI api = new ContainerAdvancedAPI();
+        String name = uniqueName("test-user");
+        CreateContainerResponse container = api.createContainerWithUser(
+                "amazoncorretto:17", name, "1000:1000", "/tmp", true);
+        assertNotNull(container.getId());
+
+        // 验证用户、工作目录和只读根文件系统均已生效
+        InspectContainerResponse inspect = api.inspectContainer(container.getId());
+        assertEquals("1000:1000", inspect.getConfig().getUser(), "用户应为 1000:1000");
+        assertEquals("/tmp", inspect.getConfig().getWorkingDir(), "工作目录应为 /tmp");
+        assertTrue(inspect.getHostConfig().getReadonlyRootfs(), "根文件系统应为只读");
+
+        api.removeContainer(container.getId());
+    }
+
+    /**
+     * 测试 nginx 端口映射容器可被外部访问(-p 8080:80,HTTP 200)
+     */
+    @Test
+    void nginxWithPortMappingCanBeAccessed() throws IOException, InterruptedException {
+        ContainerAdvancedAPI api = new ContainerAdvancedAPI();
+        String name = uniqueName("test-nginx");
+        int hostPort = 18080;
+
+        api.createAndStartNginx(name, hostPort);
+
+        // 等待 nginx 启动完成
+        String url = "http://localhost:" + hostPort + "/";
+        HttpResponse<String> response = null;
+        for (int i = 0; i < 10; i++) {
+            try {
+                HttpClient client = HttpClient.newBuilder()
+                        .connectTimeout(Duration.ofSeconds(2))
+                        .build();
+                HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
+                response = client.send(request, HttpResponse.BodyHandlers.ofString());
+                break;
+            } catch (IOException e) {
+                Thread.sleep(1000);
+            }
+        }
+
+        assertNotNull(response, "nginx 应在超时时间内返回响应");
+        assertEquals(200, response.statusCode(), "nginx 首页应返回 HTTP 200");
+
+        // 清理运行中的容器
+        var containers = api.inspectContainer(name);
+        api.removeContainer(containers.getId());
+    }
+
+    /**
+     * 测试生产风格容器:重启策略 + 资源限制 + 用户 + 端口映射 + 只读文件系统综合生效
+     */
+    @Test
+    void productionStyleContainer() {
+        ContainerAdvancedAPI api = new ContainerAdvancedAPI();
+        String name = uniqueName("test-prod");
+        String id = api.createProductionStyleContainer(name);
+        assertNotNull(id);
+
+        InspectContainerResponse inspect = api.inspectContainer(id);
+        HostConfigAssert.assertAll(inspect);
+
+        api.removeContainer(id);
+    }
+
+    /**
+     * 生产风格容器的配置断言辅助类(内嵌静态断言,方便复用)
+     */
+    private static class HostConfigAssert {
+        static void assertAll(InspectContainerResponse inspect) {
+            assertEquals("unless-stopped", inspect.getHostConfig().getRestartPolicy().getName(),
+                    "生产容器应配置 unless-stopped 重启策略");
+            assertEquals(128 * 1024 * 1024L, inspect.getHostConfig().getMemory(),
+                    "生产容器应限制内存 128MB");
+            assertEquals(500_000_000L, inspect.getHostConfig().getNanoCPUs(),
+                    "生产容器应限制 0.5 核 CPU");
+            assertTrue(inspect.getHostConfig().getReadonlyRootfs(), "生产容器根文件系统应只读");
+            assertEquals("1000:1000", inspect.getConfig().getUser(), "生产容器应以非 root 用户运行");
+        }
+    }
+}

+ 141 - 0
src/test/java/space/anyi/docker/VolumeNetworkAPITest.java

@@ -0,0 +1,141 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectVolumeResponse;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 数据卷与网络管理 API 的测试类
+ * 注意:需要本机 Docker daemon 运行中
+ * 使用 amazoncorretto:17 / nginx:latest 镜像,每个测试自行清理
+ */
+class VolumeNetworkAPITest {
+
+    private static final Logger log = LoggerFactory.getLogger(VolumeNetworkAPITest.class);
+
+    private String uniqueName(String prefix) {
+        return prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
+    }
+
+    /**
+     * 数据卷生命周期:创建 -> 列表 -> 查看详情 -> 删除
+     */
+    @Test
+    void volumeLifecycle() {
+        VolumeNetworkAPI api = new VolumeNetworkAPI();
+        String volumeName = uniqueName("test-vol");
+
+        // 创建卷
+        String created = api.createVolume(volumeName);
+        assertEquals(volumeName, created);
+
+        // 列表应包含新建的卷
+        List<InspectVolumeResponse> volumes = api.listVolumes();
+        assertTrue(volumes.stream().anyMatch(v -> volumeName.equals(v.getName())),
+                "列表中应包含新创建的卷");
+
+        // 查看详情:挂载点应非空
+        InspectVolumeResponse details = api.inspectVolume(volumeName);
+        assertNotNull(details.getMountpoint(), "数据卷应有挂载点");
+        assertTrue(details.getMountpoint().startsWith("/var/lib/docker/volumes/"),
+                "挂载点应在 docker 卷目录下");
+
+        // 删除卷
+        api.removeVolume(volumeName);
+    }
+
+    /**
+     * 数据卷持久化验证:
+     * 容器A写入数据 -> 删除容器A -> 容器B挂载同一卷 -> 数据仍然可读
+     */
+    @Test
+    void volumeDataPersistsAcrossContainers() {
+        VolumeNetworkAPI api = new VolumeNetworkAPI();
+        String volumeName = uniqueName("test-persist");
+
+        api.createVolume(volumeName);
+
+        // 1. 容器 A 挂载卷,写入一条数据
+        String containerA = uniqueName("vol-a");
+        CreateContainerResponse a = api.createContainerWithVolume(volumeName, containerA);
+        String writeResult = api.execShellInContainer(a.getId(), "echo hello-vol > /data/file.txt");
+        log.info("容器A写入结果: {}", writeResult);
+
+        // 2. 删除容器 A(卷不删除,数据保留)
+        api.removeContainer(a.getId());
+
+        // 3. 容器 B 挂载同一个卷,读取数据
+        String containerB = uniqueName("vol-b");
+        CreateContainerResponse b = api.createContainerWithVolume(volumeName, containerB);
+        String readResult = api.execShellInContainer(b.getId(), "cat /data/file.txt");
+        assertTrue(readResult.contains("hello-vol"),
+                "容器B应能读到容器A写入的数据,实际: " + readResult);
+
+        // 清理
+        api.removeContainer(b.getId());
+        api.removeVolume(volumeName);
+    }
+
+    /**
+     * 网络生命周期:创建 -> 列表 -> 承接容器 -> 删除网络
+     */
+    @Test
+    void networkLifecycle() {
+        VolumeNetworkAPI api = new VolumeNetworkAPI();
+        String networkName = uniqueName("test-net");
+
+        // 创建网络
+        String networkId = api.createNetwork(networkName);
+        assertNotNull(networkId);
+
+        // 创建并接入网络的容器
+        String containerName = uniqueName("net-app");
+        String containerId = api.startContainerOnNetwork("nginx:latest", containerName, networkName);
+        assertNotNull(containerId);
+
+        // 列表应包含新网络
+        api.inspectNetwork(networkId);
+
+        // 清理:删除容器后删除网络
+        api.removeContainer(containerId);
+        api.removeNetwork(networkName);
+    }
+
+    /**
+     * 自定义网络内的容器可以通过容器名(DNS)互相访问:
+     * 等价命令 dns 验证: docker exec app-b getent hosts app-a
+     */
+    @Test
+    void containerToContainerDnsOnCustomNetwork() {
+        VolumeNetworkAPI api = new VolumeNetworkAPI();
+        String networkName = uniqueName("test-dns-net");
+
+        // 创建自定义网络
+        String networkId = api.createNetwork(networkName);
+
+        // 两个容器加入同一网络
+        String containerAName = uniqueName("dns-a");
+        String containerBName = uniqueName("dns-b");
+        String containerA = api.startContainerOnNetwork("nginx:latest", containerAName, networkName);
+        String containerB = api.startContainerOnNetwork("nginx:latest", containerBName, networkName);
+
+        // 在容器B中通过容器名解析容器A(Docker 内嵌 DNS 生效)
+        // 等价命令: docker exec <containerB> getent hosts <containerAName>
+        String resolveResult = api.execShellInContainer(containerB, "getent hosts " + containerAName);
+        log.info("容器B解析容器A的结果:\n{}", resolveResult);
+        assertTrue(resolveResult.contains(containerAName),
+                "自定义网络中容器名应可解析,实际: " + resolveResult);
+
+        // 清理
+        api.removeContainer(containerA);
+        api.removeContainer(containerB);
+        api.removeNetwork(networkId);
+    }
+}