Преглед на файлове

feat: add Chapter 2 - DockerClient creation, connect, pull image, create/start container

yangyi преди 2 дни
родител
ревизия
db96cc8c0a

+ 342 - 1
docker-java-sdk-tutorial.md

@@ -147,4 +147,345 @@ ExecStart=/usr/bin/dockerd \
 
 ***
 
-下一章,我们将正式开始使用 Docker Java SDK,创建 DockerClient 并连接到 Docker daemon。
+## 快速入门
+
+前面我们完成了 Docker 环境的配置,现在让我们正式开始使用 Docker Java SDK。本章将介绍如何创建 DockerClient、连接到 Docker daemon、拉取镜像,以及创建并启动一个容器。
+
+### Maven 依赖
+
+首先,我们需要在项目中引入 Docker Java SDK 的依赖。在 `pom.xml` 中添加以下配置:
+
+```xml
+<dependencies>
+    <!-- Docker Java SDK 核心库 -->
+    <dependency>
+        <groupId>com.github.docker-java</groupId>
+        <artifactId>docker-java</artifactId>
+        <version>3.7.1</version>
+    </dependency>
+    <!-- HTTP Client 传输层实现 -->
+    <dependency>
+        <groupId>com.github.docker-java</groupId>
+        <artifactId>docker-java-transport-httpclient5</artifactId>
+        <version>3.7.1</version>
+    </dependency>
+    <!-- 日志实现 -->
+    <dependency>
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-classic</artifactId>
+        <version>1.5.38</version>
+    </dependency>
+</dependencies>
+```
+
+这里我们需要两个核心依赖:`docker-java` 是 SDK 的核心库,定义了所有 API 接口和模型;`docker-java-transport-httpclient5` 是传输层实现,负责与 Docker daemon 进行 HTTP 通信。
+
+### 创建 DockerClient
+
+DockerClient 是 Docker Java SDK 的核心入口类,所有的 Docker 操作都通过它来执行。SDK 提供了两种创建方式:使用默认配置和自定义配置。
+
+#### 使用默认配置
+
+最简单的方式是使用 `DockerClientBuilder` 的默认配置,它会自动检测本机的 Docker 环境:
+
+```java
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.core.DockerClientBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Docker Java SDK 快速入门示例
+ * 演示如何使用默认配置创建 DockerClient
+ */
+public class QuickStart {
+    private static final Logger log = LoggerFactory.getLogger(QuickStart.class);
+
+    /**
+     * 使用默认配置连接本地 Docker
+     * 默认配置会自动使用 Unix socket 连接本机 Docker daemon
+     */
+    public void quickStart() {
+        // 使用默认配置创建 DockerClient 实例
+        // 内部会自动检测 Docker 环境并使用 unix:///var/run/docker.sock 连接
+        DockerClient dockerClient = DockerClientBuilder.getInstance().build();
+
+        // 使用 ping 命令测试连接是否成功
+        dockerClient.pingCmd().exec();
+        log.info("Docker client ping 成功!");
+
+        // 列出所有本地镜像,验证连接正常
+        dockerClient.listImagesCmd().exec().forEach(image -> {
+            log.info("Docker 镜像信息: {}", image);
+        });
+    }
+}
+```
+
+这段代码展示了最基本的使用方式:通过 `DockerClientBuilder.getInstance().build()` 创建客户端,然后使用 `pingCmd()` 测试连接,最后列出所有镜像验证功能正常。
+
+**思考:** `DockerClientBuilder.getInstance()` 内部做了什么?它是如何知道 Docker daemon 的地址的?
+
+实际上,`DockerClientBuilder.getInstance()` 会读取环境变量 `DOCKER_HOST` 来确定 Docker daemon 的地址。如果该环境变量未设置,则默认使用 `unix:///var/run/docker.sock`。
+
+#### 使用自定义配置
+
+在实际项目中,我们通常需要自定义连接参数,比如指定 Docker daemon 地址、设置超时时间等:
+
+```java
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.core.DockerClientImpl;
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientConfig;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import com.github.dockerjava.transport.DockerHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+
+/**
+ * Docker Java SDK 自定义配置示例
+ * 演示如何使用自定义参数创建 DockerClient
+ */
+public class QuickStart {
+    private static final Logger log = LoggerFactory.getLogger(QuickStart.class);
+
+    // 连接地址配置
+    private static final String UNIX_HOST = "unix://var/run/docker.sock";
+    private static final String TCP_HOST = "tcp://0.0.0.0:2375";
+
+    /**
+     * 使用自定义配置连接 Docker
+     * 可以指定连接地址、超时时间、最大连接数等参数
+     */
+    public void customStart() {
+        // 步骤1:构建 DockerClientConfig 配置对象
+        DockerClientConfig dockerClientConfig = DefaultDockerClientConfig
+                .createDefaultConfigBuilder()
+                .withDockerHost(UNIX_HOST)  // 指定 Docker daemon 地址
+                // 以下为可选的 TLS 配置
+                //.withDockerTlsVerify(true)
+                //.withDockerCertPath("/home/user/.docker")
+                // 以下为可选的 Registry 认证配置
+                //.withRegistryUsername(registryUser)
+                //.withRegistryPassword(registryPass)
+                //.withRegistryEmail(registryMail)
+                //.withRegistryUrl(registryUrl)
+                .build();
+        log.info("Docker 客户端配置: {}", dockerClientConfig);
+
+        // 步骤2:构建 HTTP 传输层实例
+        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
+                .dockerHost(dockerClientConfig.getDockerHost())  // 使用配置中的地址
+                //.sslConfig(dockerClientConfig.getSSLConfig())   // SSL 配置
+                .maxConnections(100)           // 最大连接数
+                .connectionTimeout(Duration.ofSeconds(30))  // 连接超时时间
+                .responseTimeout(Duration.ofSeconds(45))    // 响应超时时间
+                .build();
+        log.info("HTTP 客户端配置: {}", httpClient);
+
+        // 步骤3:根据自定义配置创建 DockerClient
+        DockerClient dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient);
+
+        // 测试连接
+        dockerClient.pingCmd().exec();
+        log.info("自定义配置连接成功!");
+
+        // 列出所有镜像
+        dockerClient.listImagesCmd().exec().forEach(image -> {
+            log.info("Docker 镜像信息: {}", image);
+        });
+    }
+}
+```
+
+自定义配置的创建分为三个步骤:
+1. 构建 `DockerClientConfig`:配置 Docker daemon 地址、TLS、Registry 认证等
+2. 构建 `DockerHttpClient`:配置传输层参数,如连接数、超时时间等
+3. 使用 `DockerClientImpl.getInstance()` 创建客户端实例
+
+### 连接测试
+
+创建 DockerClient 后,我们可以使用 `pingCmd()` 方法测试连接是否正常。如果连接失败,SDK 会抛出 `DockerException` 异常:
+
+```java
+try {
+    dockerClient.pingCmd().exec();
+    log.info("Docker 连接成功!");
+} catch (Exception e) {
+    log.error("Docker 连接失败: {}", e.getMessage());
+}
+```
+
+我们还可以获取 Docker daemon 的版本信息,进一步验证连接:
+
+```java
+// 获取 Docker 版本信息
+String version = dockerClient.versionCmd().exec().getVersion();
+log.info("Docker 版本: {}", version);
+
+// 获取 Docker 系统信息
+Info info = dockerClient.infoCmd().exec();
+log.info("Docker 系统信息: {}", info);
+```
+
+### 拉取镜像
+
+拉取镜像是使用 Docker 的基本操作之一。SDK 提供了 `pullImageCmd()` 方法来拉取镜像:
+
+```java
+import com.github.dockerjava.api.command.PullImageCmd;
+import com.github.dockerjava.api.command.PullImageResultCallback;
+import com.github.dockerjava.api.model.PullResponseItem;
+
+/**
+ * 拉取 Docker 镜像
+ * @param imageName 镜像名称,如 "hello-world" 或 "nginx:latest"
+ */
+public void pullImage(String imageName) {
+    try {
+        // 构建并执行拉取命令
+        // exec() 方法会阻塞直到镜像拉取完成
+        dockerClient.pullImageCmd(imageName)
+                .exec(new PullImageResultCallback())
+                .awaitCompletion();
+        
+        log.info("镜像 {} 拉取成功!", imageName);
+    } catch (InterruptedException e) {
+        log.error("镜像拉取被中断: {}", e.getMessage());
+        Thread.currentThread().interrupt();
+    }
+}
+```
+
+**注意:** `PullImageResultCallback` 是一个异步回调类,我们需要调用 `awaitCompletion()` 方法来等待拉取完成。如果不调用此方法,拉取操作会在后台异步执行,主线程可能无法获取到结果。
+
+### 创建容器并启动
+
+拉取镜像后,我们可以使用该镜像创建并启动一个容器。这个过程分为两步:先创建容器(create),再启动容器(start):
+
+```java
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.Ports;
+
+/**
+ * 创建并启动一个容器
+ * @param imageName 镜像名称
+ * @param containerName 容器名称
+ */
+public void createAndStartContainer(String imageName, String containerName) {
+    // 步骤1:创建容器
+    // 创建容器只是分配资源和配置,并不会真正运行
+    CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
+            .withName(containerName)  // 设置容器名称
+            .exec();
+
+    log.info("容器创建成功,ID: {}", container.getId());
+
+    // 步骤2:启动容器
+    dockerClient.startContainerCmd(container.getId()).exec();
+    log.info("容器 {} 启动成功!", containerName);
+}
+```
+
+创建容器时,我们可以通过链式调用设置各种参数:
+
+```java
+// 创建一个带有端口映射和环境变量的容器
+CreateContainerResponse container = dockerClient.createContainerCmd("nginx:latest")
+        .withName("my-nginx")
+        .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80")  // 设置环境变量
+        .withExposedPorts(ExposedPort.tcp(80))  // 暴露端口
+        .withPortBindings(new Ports.Binding(8080, 80))  // 端口映射
+        .exec();
+```
+
+### 完整示例
+
+下面是一个完整的示例,展示了从创建 DockerClient 到运行容器的整个流程:
+
+```java
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientImpl;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import com.github.dockerjava.transport.DockerHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+
+/**
+ * Docker Java SDK 快速入门完整示例
+ */
+public class QuickStart {
+    private static final Logger log = LoggerFactory.getLogger(QuickStart.class);
+
+    private final DockerClient dockerClient;
+
+    public QuickStart() {
+        // 创建 DockerClient 实例
+        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
+        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
+                .dockerHost(config.getDockerHost())
+                .maxConnections(100)
+                .connectionTimeout(Duration.ofSeconds(30))
+                .responseTimeout(Duration.ofSeconds(45))
+                .build();
+        this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
+    }
+
+    /**
+     * 完整流程演示:连接 -> 拉取镜像 -> 创建容器 -> 启动容器
+     */
+    public void runDemo() {
+        // 1. 测试连接
+        dockerClient.pingCmd().exec();
+        log.info("1. Docker 连接成功");
+
+        // 2. 拉取 hello-world 镜像
+        try {
+            dockerClient.pullImageCmd("hello-world")
+                    .exec(new com.github.dockerjava.api.command.PullImageResultCallback())
+                    .awaitCompletion();
+            log.info("2. 镜像拉取成功");
+        } catch (InterruptedException e) {
+            log.error("镜像拉取被中断");
+            Thread.currentThread().interrupt();
+            return;
+        }
+
+        // 3. 创建并启动容器
+        CreateContainerResponse container = dockerClient.createContainerCmd("hello-world")
+                .withName("demo-container")
+                .exec();
+        log.info("3. 容器创建成功,ID: {}", container.getId());
+
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("4. 容器启动成功");
+    }
+
+    public static void main(String[] args) {
+        new QuickStart().runDemo();
+    }
+}
+```
+
+运行这段代码,你会看到 Docker 客户端成功连接、拉取镜像、创建并启动容器的完整过程。
+
+**思考:** 为什么创建容器和启动容器要分成两步?直接一步完成不是更简单吗?
+
+将创建和启动分开的设计允许我们在容器启动前进行更多配置,比如设置网络、挂载卷、配置资源限制等。这种设计也符合 Docker 的命令行操作习惯(`docker create` + `docker start`)。
+
+***
+
+下一章,我们将深入学习镜像管理相关的 API,包括列出镜像、查看详情、删除镜像等操作。

+ 39 - 0
src/main/java/space/anyi/docker/DockerClientFactory.java

@@ -0,0 +1,39 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientConfig;
+import com.github.dockerjava.core.DockerClientImpl;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import com.github.dockerjava.transport.DockerHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+
+/**
+ * Docker 镜像管理 API 示例
+ * 提供 DockerClient 的通用封装,供其他示例类复用
+ */
+public class DockerClientFactory {
+    private static final Logger log = LoggerFactory.getLogger(DockerClientFactory.class);
+
+    /**
+     * 创建一个 DockerClient 实例
+     * 使用默认配置 + 自定义 HTTP 客户端参数
+     */
+    public static DockerClient createDockerClient() {
+        DockerClientConfig dockerClientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
+                .build();
+        log.info("Docker 客户端配置: {}", dockerClientConfig);
+        // 构建 HTTP 传输层实例
+        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
+                .dockerHost(dockerClientConfig.getDockerHost())
+                .maxConnections(100)
+                .connectionTimeout(Duration.ofSeconds(30))
+                .responseTimeout(Duration.ofSeconds(45))
+                .build();
+        return DockerClientImpl.getInstance(dockerClientConfig, httpClient);
+    }
+}

+ 49 - 0
src/main/java/space/anyi/docker/ImageManageAPI.java

@@ -0,0 +1,49 @@
+package space.anyi.docker;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.PullImageResultCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Docker 镜像管理 API 示例
+ * 演示拉取、列出、删除等镜像操作
+ */
+public class ImageManageAPI {
+    private static final Logger log = LoggerFactory.getLogger(ImageManageAPI.class);
+
+    private final DockerClient dockerClient;
+
+    public ImageManageAPI() {
+        // 使用工厂类创建统一的 DockerClient 实例
+        this.dockerClient = DockerClientFactory.createDockerClient();
+    }
+
+    /**
+     * 拉取指定名称的镜像
+     * 优化:如果本地已存在同名镜像则直接使用,无需访问网络
+     * @param imageName 镜像名称,如 "hello-world" 或 "nginx:latest"
+     */
+    public boolean pullImage(String imageName) {
+        // 先检查本地是否已存在该镜像,避免无谓的网络访问
+        boolean exists = dockerClient.listImagesCmd().exec().stream()
+                .anyMatch(img -> img.getRepoTags() != null &&
+                        java.util.Arrays.asList(img.getRepoTags()).contains(imageName));
+        if (exists) {
+            log.info("镜像 {} 已存在于本地,跳过拉取", imageName);
+            return true;
+        }
+        try {
+            // exec() 执行命令,awaitCompletion() 阻塞等待镜像拉取完成
+            dockerClient.pullImageCmd(imageName)
+                    .exec(new PullImageResultCallback())
+                    .awaitCompletion();
+            log.info("镜像 {} 拉取成功!", imageName);
+            return true;
+        } catch (InterruptedException e) {
+            log.error("镜像拉取被中断: {}", e.getMessage());
+            Thread.currentThread().interrupt();
+            return false;
+        }
+    }
+}

+ 189 - 8
src/main/java/space/anyi/docker/QuickStart.java

@@ -1,12 +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.PullImageResultCallback;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.Ports;
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.core.DockerClientConfig;
+import com.github.dockerjava.core.DockerClientImpl;
+import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
+import com.github.dockerjava.transport.DockerHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+
 /**
- * @fileName: QuickSatrt
- * @projectName: docker-sdk-demo
- * @package: space.anyi.docker
- * @author: yangyi
- * @date:15/9/2026 7:56 pm
- * @description: TODO
+ * Docker Java SDK 快速入门示例
+ * 演示 DockerClient 的创建、连接、拉取镜像、创建并启动容器等基本操作
  */
-public class QuickSatrt {
-}
+public class QuickStart {
+    private static final Logger log = LoggerFactory.getLogger(QuickStart.class);
+
+    private final DockerClient dockerClient;
+
+    public QuickStart() {
+        // 创建 DockerClient 实例(使用自定义配置)
+        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
+        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
+                .dockerHost(config.getDockerHost())
+                .maxConnections(100)
+                .connectionTimeout(Duration.ofSeconds(30))
+                .responseTimeout(Duration.ofSeconds(45))
+                .build();
+        this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
+    }
+
+    /**
+     * 使用默认的配置连接本地docker
+     * 通过 DockerClientBuilder 自动检测 Docker 环境
+     */
+    public void quickStart() {
+        DockerClient defaultClient = DockerClientBuilder.getInstance().build();
+        defaultClient.pingCmd().exec();
+        log.info("Docker client ping 成功!");
+        defaultClient.listImagesCmd().exec().forEach(image -> {
+            log.info("Docker 镜像信息: {}", image);
+        });
+    }
+
+    /**
+     * 使用显式指定配置连接 docker
+     * 可以自定义连接地址、TLS、超时时间等参数
+     */
+    public void customStart() {
+        final String UNIX_HOST = "unix://var/run/docker.sock";
+        final String TCP_HOST = "tcp://0.0.0.0:2375";
+        // 构建 DockerClientConfig 配置对象
+        DockerClientConfig dockerClientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
+                .withDockerHost(UNIX_HOST)
+                //.withDockerTlsVerify(true)
+                //.withDockerCertPath("/home/user/.docker")
+                //.withRegistryUsername(registryUser)
+                //.withRegistryPassword(registryPass)
+                //.withRegistryEmail(registryMail)
+                //.withRegistryUrl(registryUrl)
+                .build();
+        log.info("Docker 客户端配置: {}", dockerClientConfig);
+        // 构建 HTTP 传输层实例
+        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
+                .dockerHost(dockerClientConfig.getDockerHost())
+                //.sslConfig(dockerClientConfig.getSSLConfig())
+                .maxConnections(100)
+                .connectionTimeout(Duration.ofSeconds(30))
+                .responseTimeout(Duration.ofSeconds(45))
+                .build();
+        log.info("HTTP 客户端配置: {}", httpClient);
+        // 根据自定义配置创建 DockerClient
+        DockerClient customClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient);
+        customClient.pingCmd().exec();
+        customClient.listImagesCmd().exec().forEach(image -> {
+            log.info("Docker 镜像信息: {}", image);
+        });
+    }
+
+    /**
+     * 拉取指定名称的镜像
+     * 优化:如果本地已存在同名镜像则直接使用,无需访问网络
+     * @param imageName 镜像名称,如 "hello-world" 或 "nginx:latest"
+     */
+    public void pullImage(String imageName) {
+        // 先检查本地是否已存在该镜像,避免无谓的网络访问
+        boolean exists = dockerClient.listImagesCmd().exec().stream()
+                .anyMatch(img -> img.getRepoTags() != null &&
+                        java.util.Arrays.asList(img.getRepoTags()).contains(imageName));
+        if (exists) {
+            log.info("镜像 {} 已存在于本地,跳过拉取", imageName);
+            return;
+        }
+        try {
+            // exec() 执行命令,awaitCompletion() 阻塞等待镜像拉取完成
+            dockerClient.pullImageCmd(imageName)
+                    .exec(new PullImageResultCallback())
+                    .awaitCompletion();
+            log.info("镜像 {} 拉取成功!", imageName);
+        } catch (InterruptedException e) {
+            log.error("镜像拉取被中断: {}", e.getMessage());
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * 创建并启动一个容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     */
+    public void createAndStartContainer(String imageName, String containerName) {
+        // 步骤1:创建容器(只分配资源和配置,不真正运行)
+        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .exec();
+        log.info("容器创建成功,ID: {}", container.getId());
+
+        // 步骤2:启动容器
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("容器 {} 启动成功!", containerName);
+    }
+
+    /**
+     * 创建并启动一个带端口映射的容器
+     * @param imageName 镜像名称
+     * @param containerName 容器名称
+     * @param hostPort 宿主机端口
+     * @param containerPort 容器内端口
+     */
+    public void createAndStartContainerWithPort(String imageName, String containerName,
+                                                 Integer hostPort, Integer containerPort) {
+        // 暴露容器内端口
+        ExposedPort exposedPort = ExposedPort.tcp(containerPort);
+        // 配置端口绑定(宿主机端口 -> 容器内端口)
+        Ports portBindings = new Ports();
+        portBindings.bind(exposedPort, Ports.Binding.bindPort(hostPort));
+
+        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
+                .withName(containerName)
+                .withExposedPorts(exposedPort)
+                .withPortBindings(portBindings)
+                .exec();
+        log.info("容器创建成功,ID: {}, 端口映射: {} -> {}", container.getId(), hostPort, containerPort);
+
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("容器 {} 启动成功!", containerName);
+    }
+
+    /**
+     * 完整流程安全演示:连接 -> 检查镜像 -> 创建容器 -> 启动容器 -> 清理
+     * 使用本地已有镜像避免网络依赖
+     */
+    public void runDemo() {
+        // 1. 测试连接
+        dockerClient.pingCmd().exec();
+        log.info("1. Docker 连接成功");
+
+        // 2. 检查本地是否已有可用的镜像
+        String imageName = "alist666/alist:latest";
+        boolean hasImage = dockerClient.listImagesCmd().exec().stream()
+                .anyMatch(img -> img.getRepoTags() != null &&
+                        java.util.Arrays.asList(img.getRepoTags()).contains(imageName));
+        if (!hasImage) {
+            log.warn("本地不存在镜像 {},跳过容器演示", imageName);
+            return;
+        }
+        log.info("2. 找到本地镜像: {}", imageName);
+
+        // 3. 创建容器
+        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
+                .withName("demo-container-" + System.currentTimeMillis())
+                .exec();
+        log.info("3. 容器创建成功,ID: {}", container.getId());
+
+        // 4. 启动容器
+        dockerClient.startContainerCmd(container.getId()).exec();
+        log.info("4. 容器启动成功");
+
+        // 5. 清理容器(避免污染环境)
+        dockerClient.removeContainerCmd(container.getId())
+                .withForce(true)
+                .exec();
+        log.info("5. 容器已清理");
+    }
+}

+ 25 - 0
src/test/java/space/anyi/docker/ImageManageAPITest.java

@@ -0,0 +1,25 @@
+package space.anyi.docker;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 镜像管理 API 的测试类
+ * 注意:这些测试需要本机 Docker daemon 运行中,属于集成测试
+ */
+class ImageManageAPITest {
+
+    // 本地已有镜像,避免依赖网络拉取
+    private static final String LOCAL_IMAGE = "alist666/alist:latest";
+
+    /**
+     * 测试拉取本地已有镜像(走本地缓存,不访问网络)
+     */
+    @Test
+    void pullImage() {
+        ImageManageAPI api = new ImageManageAPI();
+        assertTrue(api.pullImage(LOCAL_IMAGE),
+                "拉取本地已有镜像应该返回 true");
+    }
+}

+ 57 - 6
src/test/java/space/anyi/docker/QuickStartTest.java

@@ -5,16 +5,67 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
- * @fileName: QuickStartTest
- * @projectName: docker-sdk-demo
- * @package: space.anyi.docker
- * @author: yangyi
- * @date:15/9/2026 9:38 pm
- * @description: TODO
+ * QuickStart 快速入门功能的测试类
+ * 注意:这些测试需要本机 Docker daemon 运行中,属于集成测试
+ *
+ * 测试策略:
+ * 1. 连接测试(quickStart/customStart)只依赖本地 Docker daemon
+ * 2. pullImage 走本地镜像缓存,不访问外部网络
+ * 3. 容器相关测试使用本地已有镜像
  */
 class QuickStartTest {
 
+    // 本地已有镜像,避免依赖网络拉取
+    private static final String LOCAL_IMAGE = "alist666/alist:latest";
+
+    /**
+     * 测试使用默认配置连接 Docker
+     */
     @Test
     void quickStart() {
+        assertDoesNotThrow(() -> new QuickStart().quickStart(),
+                "使用默认配置连接 Docker 应该成功");
+    }
+
+    /**
+     * 测试使用自定义配置连接 Docker
+     */
+    @Test
+    void customStart() {
+        assertDoesNotThrow(() -> new QuickStart().customStart(),
+                "使用自定义配置连接 Docker 应该成功");
+    }
+
+    /**
+     * 测试拉取本地已有镜像(走本地缓存,不访问网络)
+     */
+    @Test
+    void pullImage() {
+        QuickStart quickStart = new QuickStart();
+        assertDoesNotThrow(() -> quickStart.pullImage(LOCAL_IMAGE),
+                "拉取本地已有镜像应该成功");
+    }
+
+    /**
+     * 测试创建并启动容器(使用本地已有镜像)
+     */
+    @Test
+    void createAndStartContainer() {
+        QuickStart quickStart = new QuickStart();
+        String containerName = "test-container-" + System.currentTimeMillis();
+        assertDoesNotThrow(() -> quickStart.createAndStartContainer(LOCAL_IMAGE, containerName),
+                "创建并启动容器应该成功");
+    }
+
+    /**
+     * 测试创建带端口映射的容器(使用本地已有镜像)
+     */
+    @Test
+    void createAndStartContainerWithPort() {
+        QuickStart quickStart = new QuickStart();
+        String containerName = "test-nginx-" + System.currentTimeMillis();
+        // alist 默认使用 5244 端口
+        assertDoesNotThrow(() -> quickStart.createAndStartContainerWithPort(LOCAL_IMAGE, containerName, 18080, 5244),
+                "创建带端口映射的容器应该成功");
     }
 }