docker-java-sdk-tutorial.md 56 KB

Docker Java SDK 使用教程

Solomon Hykes — Docker 创始人

Docker 提供了一个核心的抽象层,让你可以在任何基础设施上运行任何应用。这种抽象层的本质是:将应用的运行环境打包成一个标准化的单元,这个单元可以在任何地方运行。

那么,从这章开始,就让我们来感受一下,Docker Java SDK 为我们带来了什么。


前置Docker配置

在使用 Docker Java SDK 之前,我们需要确保本机的 Docker 环境已经正确配置。SDK 通过 Unix socket 或 TCP 协议与 Docker daemon 通信,因此需要根据实际情况选择合适的连接方式并完成相应配置。

用户组配置

默认情况下,Docker daemon 的 Unix socket 文件 /var/run/docker.sock 的所有者是 root 用户,所属组是 docker。如果当前用户没有 docker 用户组的权限,在连接 Docker 时会遇到权限拒绝的问题:

permission denied while trying to connect to the Docker daemon socket

解决方法是将当前用户添加到 docker 用户组中:

# 将当前用户添加到 docker 用户组
sudo usermod -aG docker $USER

# 查看当前用户所属 groups
groups

# 如果 usermod 不生效,可以尝试 newgrp 刷新组信息
newgrp docker

添加完成后,需要重新登录系统重启系统让修改生效。我们可以通过以下命令验证是否配置成功:

# 查看 docker.sock 文件的权限
ls -la /var/run/docker.sock

# 输出示例:srw-rw---- 1 root docker 0 Sep 15 10:00 /var/run/docker.sock
# 注意权限中的 's' 表示 socket 文件,所属组为 docker

思考: 为什么 Docker 选择使用 Unix socket 而不是普通的文件来进行通信?

Unix socket 是一种进程间通信(IPC)机制,相比 TCP 连接,它在同一台机器上的通信效率更高,且不需要经过网络协议栈,安全性也更好。因此 Docker daemon 默认监听 Unix socket 作为主要的通信方式。

网络协议配置

Docker Java SDK 支持两种连接协议:Unix socketTCP

Unix socket 协议

Unix socket 是默认的连接方式,适用于 Docker daemon 运行在本机的场景。SDK 通过以下地址连接:

unix:///var/run/docker.sock

这种方式的优点是无需额外配置,安全性高(通过文件权限控制访问),但缺点是只能连接本机的 Docker daemon。

TCP 协议

当需要远程连接 Docker daemon 时,需要使用 TCP 协议。这要求 Docker daemon 启动时开启 TCP 端口监听。

方法一:修改 Docker daemon 启动参数

/etc/docker/daemon.json 中添加配置:

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}

方法二:修改 systemd service 配置

创建或编辑 systemd 配置文件:

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vi /etc/systemd/system/docker.service.d/override.conf

添加以下内容:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock

然后重启 Docker 服务:

sudo systemctl daemon-reload
sudo systemctl restart docker

验证 TCP 监听是否生效:

# 查看 Docker daemon 监听的端口
ss -tlnp | grep 2375

# 或使用 curl 测试
curl http://localhost:2375/version

注意: 开启 TCP 监听后,任何能够访问该端口的客户端都可以操作 Docker daemon,存在严重的安全风险。在生产环境中,建议配合 TLS 证书进行加密通信。

TLS 安全通信配置

在生产环境中使用 TCP 协议时,应该启用 TLS 加密:

# 创建 TLS 证书(示例)
mkdir -p /etc/docker/certs
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 \
  -keyout /etc/docker/certs/server-key.pem \
  -out /etc/docker/certs/server-cert.pem \
  -nodes -subj "/CN=localhost"

# Docker daemon 启动参数
ExecStart=/usr/bin/dockerd \
  --tlsverify \
  --tlscacert=/etc/docker/ca.pem \
  --tlscert=/etc/docker/certs/server-cert.pem \
  --tlskey=/etc/docker/certs/server-key.pem \
  -H tcp://0.0.0.0:2376 \
  -H unix://var/run/docker.sock

连接方式对比

特性 Unix socket TCP TCP + TLS
适用场景 本机连接 远程连接 远程连接(生产)
安全性 高(文件权限) 低(明文传输) 高(证书加密)
性能 中等 中等(加密开销)
配置复杂度
默认端口 2375 2376

思考: 在微服务架构中,如果需要让应用通过 Docker Java SDK 管理容器化的服务,你会选择哪种连接方式?为什么?


快速入门

前面我们完成了 Docker 环境的配置,现在让我们正式开始使用 Docker Java SDK。本章将介绍如何创建 DockerClient、连接到 Docker daemon、拉取镜像,以及创建并启动一个容器。

Maven 依赖

首先,我们需要在项目中引入 Docker Java SDK 的依赖。在 pom.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 环境:

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 地址、设置超时时间等:

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 异常:

try {
    dockerClient.pingCmd().exec();
    log.info("Docker 连接成功!");
} catch (Exception e) {
    log.error("Docker 连接失败: {}", e.getMessage());
}

我们还可以获取 Docker daemon 的版本信息,进一步验证连接:

// 获取 Docker 版本信息
String version = dockerClient.versionCmd().exec().getVersion();
log.info("Docker 版本: {}", version);

// 获取 Docker 系统信息
Info info = dockerClient.infoCmd().exec();
log.info("Docker 系统信息: {}", info);

拉取镜像

拉取镜像是使用 Docker 的基本操作之一。SDK 提供了 pullImageCmd() 方法来拉取镜像:

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):

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);
}

创建容器时,我们可以通过链式调用设置各种参数:

// 创建一个带有端口映射和环境变量的容器
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 到运行容器的整个流程:

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

镜像(Image)是 Docker 的核心概念之一,它是一个只读的模板,包含了运行应用所需的所有内容:代码、运行时、系统工具、库和配置。Docker Java SDK 提供了一系列镜像管理 API,本章我们来逐一学习。

listImagesCmd - 列出所有镜像

listImagesCmd() 方法用于列出 Docker daemon 上的镜像列表,对应 Docker CLI 的 docker images 命令。

/**
 * 列出自定义节点上所有存在的镜像
 * @return 镜像列表
 */
public List<Image> listImages() {
    List<Image> images = dockerClient.listImagesCmd()
            .withShowAll(true)      // 展示所有镜像(包括中间层)
            .exec();
    log.info("本机共有 {} 个镜像", images.size());
    return images;
}

Image 对象包含了镜像的核心信息:

字段 说明
getId() 镜像的完整 ID(sha256 哈希值)
getRepoTags() 镜像的仓库和标签列表,如 ["nginx:latest", "nginx:1.25"]
getRepoDigests() 镜像摘要
getSize() 镜像大小(字节)
getCreated() 创建时间戳
getLabels() 镜像标签(Label)
getContainers() 使用该镜像的容器数量

思考: 为什么 listImagesCmd() 默认不包含中间层镜像?withShowAll(true) 开启了什么?

Docker 的镜像采用分层存储架构,一个镜像由多个 Layer 叠加而成。中间层镜像(dangling image)通常没有标签,它们只是构建过程中的临时产物。withShowAll(true) 会将这些中间层镜像也展示出来,方便排查磁盘占用问题。

inspectImageCmd - 获取镜像详细信息

当我们需要深入了解一个镜像的完整信息时,可以使用 inspectImageCmd() 方法:

/**
 * 获取镜像的详细信息
 * 包括镜像的 ID、仓库、标签、创建时间、大小、Layer 等
 * @param imageId 镜像 ID 或名称
 * @return 镜像详情
 */
public InspectImageResponse inspectImage(String imageId) {
    InspectImageResponse response = dockerClient.inspectImageCmd(imageId).exec();
    log.info("镜像 {} 详情: {}", imageId, response);
    return response;
}

InspectImageResponse 返回的信息比 Image 更为详细,主要包括:

字段 说明
getId() 镜像 ID
getArch() 支持的 CPU 架构
getOs() 操作系统类型
getConfig() 镜像的配置信息(包括环境变量、入口命令等)
getContainerConfig() 构建镜像时的容器配置
getRootFs() 镜像的文件系统层次
getDockerVersion() 构建镜像时使用的 Docker 版本

思考: inspectImageCmd()listImagesCmd() 的返回信息有什么区别?什么场景下需要用 inspect

listImagesCmd() 返回的是简要信息,适合批量展示;inspectImageCmd() 返回的是完整 JSON 详情,适合获取镜像的配置细节,比如环境变量、入口命令、暴露端口等。在需要根据镜像配置来决定如何启动容器时,inspect 非常有用。

pullImageCmd - 拉取镜像(异步回调机制)

pullImageCmd() 用于从远程仓库拉取镜像,对应 docker pull 命令。与前面几个方法不同,拉取镜像是一个异步操作,需要借助回调机制来处理结果:

/**
 * 拉取指定名称的镜像
 * @param imageName 镜像名称
 * @return 拉取是否成功
 */
public boolean pullImage(String imageName) {
    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;
    }
}

PullImageResultCallback 继承自 ResultCallback.Adapter,它会持续接收 Docker daemon 推送过来的拉取进度。awaitCompletion() 方法会阻塞当前线程,直到拉取完成或发生异常。

思考: 为什么要设计成异步回调模式?如果直接返回结果不是更简单吗?

镜像拉取可能是一个漫长的过程(大型镜像可能需要数分钟),如果将这个方法设计为同步阻塞,那么调用线程会被长时间占用。异步回调模式允许我们在等待拉取的同时执行其他操作,比如拉取多个镜像时,可以并发执行。

removeImageCmd - 删除镜像

删除镜像使用 removeImageCmd() 方法,对应 docker rmi 命令:

/**
 * 删除本地镜像
 * @param imageId 镜像 ID 或名称
 * @param force 是否强制删除(即使镜像被容器使用)
 */
public void removeImage(String imageId, boolean force) {
    dockerClient.removeImageCmd(imageId)
            .withForce(force)   // 强制删除
            .exec();
    log.info("镜像 {} 已删除", imageId);
}

注意: 如果镜像正在被某个容器使用,普通删除会失败。此时需要设置 force=true 强制删除,或者先删除容器再删除镜像。

tagImageCmd - 标记镜像

tagImageCmd() 用于给镜像添加标签,对应 docker tag 命令,常用于给镜像重命名或在本地建立镜像副本:

/**
 * 为镜像添加标签(相当于 docker tag)
 * @param imageId 源镜像 ID 或名称
 * @param repository 新的仓库名
 * @param tag 新的标签名
 */
public void tagImage(String imageId, String repository, String tag) {
    dockerClient.tagImageCmd(imageId, repository, tag).exec();
    log.info("镜像 {} 已标记为 {}:{}", imageId, repository, tag);
}

例如,当我们拉取了 nginx:latest 镜像后,想将其标记为 my-nginx:v1.0

api.tagImage("nginx:latest", "my-nginx", "v1.0");

saveImageCmd / loadImageCmd - 导入导出镜像

Docker 还支持将镜像导出为 tar 文件,或者从 tar 文件导入镜像。这在离线部署场景中非常有用:

// 将镜像导出为 tar 文件
public void saveImage(String imageId, String filePath) {
    try (OutputStream outputStream = new FileOutputStream(filePath)) {
        dockerClient.saveImageCmd(imageId).exec(outputStream);
        log.info("镜像 {} 已导出到 {}", imageId, filePath);
    } catch (IOException e) {
        log.error("导出镜像失败: {}", e.getMessage());
    }
}

// 从 tar 文件加载镜像
public void loadImage(String filePath) {
    try (InputStream inputStream = new FileInputStream(filePath)) {
        dockerClient.loadImageCmd(inputStream)
                .exec(new LoadImageResultCallback())
                .awaitCompletion();
        log.info("镜像已从 {} 加载成功", filePath);
    } catch (IOException | InterruptedException e) {
        log.error("加载镜像失败: {}", e.getMessage());
    }
}

思考: 镜像导出和导入在什么场景下会用到?与直接在网络上拉取镜像相比有什么优势?

镜像导出/导入适用于离线环境内网环境,比如生产环境无法直接访问 Docker Hub 时,可以在有网络的机器上导出镜像,然后通过 U 盘或内网传输到目标机器上加载。这种方式不依赖网络连接,但文件传输本身需要额外的存储空间。

镜像管理完整示例

// 列出所有镜像
List<Image> images = api.listImages();
images.forEach(img -> log.info("镜像: {}, 大小: {}MB", 
        Arrays.toString(img.getRepoTags()), img.getSize() / 1024 / 1024));

// 检查镜像是否存在
boolean exists = api.checkImageExists("nginx:latest");
if (!exists) {
    api.pullImage("nginx:latest");  // 拉取
}

// 查看镜像详情
InspectImageResponse info = api.inspectImage("nginx:latest");
log.info("镜像架构: {}, 操作系统: {}", info.getArch(), info.getOs());

// 给镜像打标签
api.tagImage("nginx:latest", "my-nginx", "v1.0");

// 删除镜像
api.removeImage("my-nginx:v1.0", false);

镜像构建相关API

前面我们学习了镜像的基本管理操作,本章将介绍如何使用 Docker Java SDK 构建自定义镜像。镜像构建是 Docker 最强大的功能之一,它允许我们将应用打包成标准化的镜像,实现"一次构建,到处运行"。

buildImageCmd - 从 Dockerfile 构建镜像

buildImageCmd() 对应 docker build 命令,它需要指定构建上下文(包含 Dockerfile 和构建所需的文件)。

/**
 * 从 Dockerfile 构建镜像
 * @param dockerfilePath Dockerfile 文件路径
 * @param imageName 构建目标镜像名称(repository:tag)
 * @return 构建成功返回镜像 ID,失败返回 null
 */
public String buildImage(String dockerfilePath, String imageName) {
    File dockerfile = new File(dockerfilePath);
    if (!dockerfile.exists()) {
        log.error("Dockerfile 不存在: {}", dockerfilePath);
        return null;
    }

    try {
        // withDockerfile 指定 Dockerfile 文件位置
        // withBaseDirectory 设置构建上下文(当前目录)
        // withTag 指定构建后的镜像名称和标签
        // exec() 返回异步回调,awaitImageId() 阻塞等待构建完成并返回镜像 ID
        String imageId = dockerClient.buildImageCmd()
                .withDockerfile(dockerfile)
                .withBaseDirectory(dockerfile.getParentFile())
                .withTag(imageName)
                .exec(new BuildImageResultCallback())
                .awaitImageId();
        log.info("镜像 {} 构建成功,镜像 ID: {}", imageName, imageId);
        return imageId;
    } catch (Exception e) {
        log.error("镜像构建失败: {}", e.getMessage());
        return null;
    }
}

构建过程的关键参数:

方法 说明
withDockerfile(File) 指定 Dockerfile 文件路径
withBaseDirectory(File) 指定构建上下文目录(Dockerfile 中 COPY 指令从此目录读取文件)
withTag(String) 指定构建出的镜像名称和标签(如 sdk-demo/hello:latest
withRemove(boolean) 构建完成后是否移除中间层容器
withForcerm(boolean) 是否强制移除中间层容器(当构建失败时)
withBuildArg(String, String) 传递构建参数(对应 ARG 指令)

BuildImageResultCallback 是构建过程的异步回调,各个方法的含义:

方法 说明
onNext(BuildResponseItem) 每收到一个构建事件时回调(如缓存命中、步骤执行等)
onError(Throwable) 构建出错时回调
awaitImageId() 阻塞等待构建完成,返回镜像 ID(超时则抛出异常)
awaitCompletion() 阻塞等待构建完成,无返回值

思考: 构建上下文(BaseDirectory)的作用是什么?为什么需要单独指定 Dockerfile 的位置?

构建上下文是 Docker 构建时能"看到"的所有文件所在的目录。当 Dockerfile 中使用 COPY app.jar /app/ 时,这个 app.jar 就是从构建上下文中寻找的。将 Dockerfile 放在构建上下文之外(通过 withDockerfile 指定)是允许的,但 COPY 指令仍然只能访问构建上下文内的文件。

构建流程详解

构建一个镜像的过程大致如下:

Dockerfile(构建指令)
    |                                   
    v                                  
构建上下文 (BaseDirectory) ──► Docker daemon 执行每个指令
    |                                   
    | 1. FROM alist666/alist:latest     ← 拉取/复用基础镜像
    | 2. RUN echo "..." > /info.txt     ← 执行命令,创建新 Layer
    |                                   
    v                                  
构建完成的镜像 (imageName:tag)          

每次 RUNCOPY 等指令都会创建一个新的镜像 Layer,所有 Layer 叠加在一起就是最终的镜像。这个分层机制使得镜像可以被高效地缓存和复用。

示例:构建一个带标识文件的自定义镜像

假设我们有如下 Dockerfile:

FROM registry.fit2cloud.com/halo/halo-pro:2.24
# 构建时添加一个标识文件,演示 Dockerfile 的 RUN 指令
RUN echo "Built by Docker Java SDK" > /build-info.txt
# 容器启动时输出构建信息
CMD ["sh", "-c", "cat /build-info.txt"]

使用我们封装好的 ImageBuildAPI 构建并验证:

ImageBuildAPI api = new ImageBuildAPI();
String imageId = api.buildImage("src/test/resources/Dockerfile", "sdk-demo/hello:latest");
log.info("构建结果: {}", imageId);

// 验证镜像已存在
List<Image> images = api.findImageByName("sdk-demo/hello:latest");
log.info("找到 {} 个匹配镜像", images.size());

运行测试可以看到构建的输出:

镜像 sdk-demo/hello:latest 构建成功,镜像 ID: sha256:xxxx
镜像 sdk-demo/hello:latest 是否存在: true

思考: 为什么 withRemove(true)withForcerm(true) 的组合在 CI/CD 流水线中很重要?

在自动化构建过程中,如果每次构建都留下大量的中间层容器,会逐渐耗尽磁盘空间。withRemove(true) 确保正常完成后清理中间层,withForcerm(true) 则在构建失败时也能强制清理,避免残留。


容器管理相关API

容器(Container)是 Docker 的另一核心概念,它是镜像的运行实例。如果说镜像是"类",那容器就是"对象"。本章将系统学习容器管理的各项 API。

容器的完整生命周期

容器的生命周期可以分为几个阶段:

创建 (create) → 启动 (start) → 运行中 (running)
                                    ↓
                    停止 (stop) / 强制终止 (kill)
                                    ↓
                              删除 (remove)

Docker 的设计哲学是:创建启动是分离的操作。创建容器只分配资源和配置,容器处于 created 状态;启动后进入 running 状态。这种分离设计让我们可以在启动前完成网络、卷、资源限制等配置。

createContainerCmd - 创建容器

/**
 * 创建容器(不启动)
 * @param imageName 镜像名称
 * @param containerName 容器名称
 * @return 创建的容器响应(包含容器 ID)
 */
public CreateContainerResponse createContainer(String imageName, String containerName) {
    // 创建容器:分配资源配置,但不会真正运行
    // withName 指定容器名称(可选,不指定时 Docker 自动生成)
    CreateContainerResponse response = dockerClient.createContainerCmd(imageName)
            .withName(containerName)
            .exec();
    log.info("容器创建成功,ID: {}, 名称: {}", response.getId(), containerName);
    return response;
}

创建容器时常用的配置项:

方法 说明
withName(String) 指定容器名称
withEnv(String...) 设置环境变量
withExposedPorts(ExposedPort...) 声明容器内暴露的端口
withPortBindings(Ports) 配置端口映射(宿主机端口 -> 容器内端口)
withCmd(String...) 覆盖镜像中定义的启动命令
withWorkingDir(String) 设置容器内工作目录
withHostConfig(HostConfig) 设置资源限制、挂载卷、网络模式等

创建带环境变量和端口映射的容器:

// 创建带环境变量的容器
dockerClient.createContainerCmd("nginx:latest")
        .withName("my-nginx")
        .withEnv("NGINX_HOST=localhost", "NGINX_PORT=80")
        .exec();

// 创建带端口映射的容器
dockerClient.createContainerCmd("nginx:latest")
        .withName("my-nginx")
        .withExposedPorts(ExposedPort.tcp(80))
        .withPortBindings(new Ports.Binding(8080, 80))
        .exec();

startContainerCmd - 启动容器

创建容器后,使用 startContainerCmd() 启动它:

/**
 * 启动容器
 * @param containerId 容器 ID
 */
public void startContainer(String containerId) {
    dockerClient.startContainerCmd(containerId).exec();
    log.info("容器 {} 启动成功", containerId);
}

思考: 在快速入门章节我们看到了 CreateContainerResponse.getId(),它返回完整的容器 ID。为什么大多数 API 都要求传容器 ID 而不是名称?

容器 ID 是容器的唯一标识,不会重复。而容器名称虽然也可以用于操作(Docker daemon 会解析),但在某些场景下名称可能不存在或重复。使用 ID 是最稳妥的方式。SDK 也支持传入短 ID 或名称,Docker daemon 会自动解析。

stopContainerCmd - 停止容器

停止容器采用"优雅退出"机制:首先向容器进程发送 SIGTERM 信号,等待一段时间后仍未退出则发送 SIGKILL 强制终止:

/**
 * 停止容器(等待容器优雅退出)
 * @param containerId 容器 ID
 * @param timeoutSeconds 等待时长(秒),超过则强制终止
 */
public void stopContainer(String containerId, Integer timeoutSeconds) {
    dockerClient.stopContainerCmd(containerId)
            .withTimeout(timeoutSeconds)  // SIGTERM 后等待时间
            .exec();
    log.info("容器 {} 已停止(等待 {} 秒)", containerId, timeoutSeconds);
}

killContainerCmd - 强制停止容器

stopContainerCmd 不同,killContainerCmd 直接发送 SIGKILL 信号,进程立即终止,不给优雅退出的机会:

/**
 * 强制停止容器(立即发送 SIGKILL)
 * @param containerId 容器 ID
 */
public void killContainer(String containerId) {
    dockerClient.killContainerCmd(containerId).exec();
    log.info("容器 {} 已被强制停止", containerId);
}

思考: 什么场景下应该用 stop,什么场景下应该用 kill?

stop 会先给进程发 SIGTERM,让应用有机会保存数据、释放资源(比如数据库需要执行清理操作);kill 则是立即杀死进程,可能导致数据丢失。生产环境中应该优先使用 stop,只有在进程无法响应或严格限时的情况下才使用 kill

removeContainerCmd - 删除容器

删除容器时需要考虑两个选项:

/**
 * 删除容器
 * @param containerId 容器 ID
 * @param force 是否强制删除(容器运行中也删除)
 * @param removeVolumes 是否同时删除关联的卷
 */
public void removeContainer(String containerId, boolean force, boolean removeVolumes) {
    dockerClient.removeContainerCmd(containerId)
            .withForce(force)            // 强制删除运行中的容器
            .withRemoveVolumes(removeVolumes)  // 删除关联数据卷
            .exec();
    log.info("容器 {} 已删除", containerId);
}

listContainersCmd - 列出容器

列出容器对应 docker ps 命令。默认只显示运行中的容器,withShowAll(true) 则显示所有容器(包括已停止的):

/**
 * 列出所有容器(包括停止的)
 * @return 容器列表
 */
public List<Container> listContainers() {
    // withShowAll(true) 展示所有容器(不只运行中的)
    List<Container> containers = dockerClient.listContainersCmd()
            .withShowAll(true)
            .exec();
    log.info("本机共有 {} 个容器", containers.size());
    return containers;
}

Container 对象包含的信息:

字段 说明
getId() 容器 ID
getNames() 容器名称列表(以 / 开头)
getImage() 使用的镜像名称
getState() 容器状态(running / exited 等)
getStatus() 状态描述(如 "Up 2 hours")
getPorts() 端口映射信息
getLabels() 容器标签

inspectContainerCmd - 获取容器详细信息

与镜像类似,容器也有详查方法。它返回容器的完整配置、网络、挂载信息:

/**
 * 获取容器详细信息
 * @param containerId 容器 ID
 * @return 容器详情
 */
public InspectContainerResponse inspectContainer(String containerId) {
    InspectContainerResponse response = dockerClient.inspectContainerCmd(containerId).exec();
    log.info("容器 {} 状态: {}, 名称: {}",
            containerId, response.getState().getStatus(), response.getName());
    return response;
}

InspectContainerResponse 的关键信息:

字段 说明
getId() 容器 ID
getName() 容器名称
getState() 容器状态(含 Running、Status、StartedAt 等)
getConfig() 容器配置(镜像、环境变量、命令等)
getNetworkSettings() 网络配置(端口映射、IP 地址等)
getMounts() 挂载的卷
getHostConfig() 宿主机相关配置
getCreated() 创建时间

完整生命周期示例

public void containerLifecycleDemo(String imageName, String containerName) {
    // 1. 创建容器
    CreateContainerResponse container = createContainer(imageName, containerName);

    // 2. 启动容器
    startContainer(container.getId());

    // 3. 查询状态
    inspectContainer(container.getId());

    // 4. 停止容器
    stopContainer(container.getId(), 10);

    // 5. 删除容器
    removeContainer(container.getId(), false, false);
    log.info("容器 {} 生命周期演示完成", containerName);
}

思考: 查看 ContainergetNames() 返回的数组——为什么是一个数组而不是单个字符串?

Docker 的设计中,一个容器可以有多个名称(别名),通常是 /容器名。当使用 --link 方式连接容器时,会为被连接的容器创建额外的名称。虽然现代 Docker 更推荐使用自定义网络,但 getNames() 的设计保留了这种能力。


容器运维相关API

前面我们学习了容器的基础管理(创建、启动、停止、删除),本章将深入讲解容器的运维操作:查看日志、在容器中执行命令、文件复制、等待容器退出等。这些操作是日常运维中最常用的功能。

logsCmd - 获取容器日志

获取容器日志对应 docker logs 命令。日志以帧(Frame)的方式流式返回,每帧包含数据类型和载荷:

/**
 * 获取容器日志(同步方式)
 * @param containerId 容器 ID
 * @param tailLines 只显示末尾的行数
 * @return 日志文本
 */
public String getContainerLogs(String containerId, int tailLines) {
    StringBuilder logBuilder = new StringBuilder();
    try {
        // withTail 只获取末尾 N 行
        LogContainerCmd cmd = dockerClient.logContainerCmd(containerId)
                .withStdOut(true)     // 获取标准输出
                .withStdErr(true)     // 获取错误输出
                .withTail(tailLines); // 只取末尾几行

        cmd.exec(new ResultCallback.Adapter<>() {
            @Override
            public void onNext(Frame frame) {
                // 日志以帧(Frame)形式返回,每帧包含不同类型的数据
                if (frame.getStreamType() == FrameType.STDOUT ||
                        frame.getStreamType() == FrameType.STDERR ||
                        frame.getStreamType() == FrameType.RAW) {
                    logBuilder.append(new String(frame.getPayload()));
                }
            }
        }).awaitCompletion();
        return logBuilder.toString();
    } catch (InterruptedException e) {
        log.error("获取容器日志被中断: {}", e.getMessage());
        Thread.currentThread().interrupt();
        return logBuilder.toString();
    }
}

常用的日志选项:

方法 说明
withStdOut(true) 获取标准输出流
withStdErr(true) 获取标准错误流
withTail(int) 只获取末尾 N 行(类似 tail -n
withSince(long) 获取某个时间戳之后的日志
withTimestamps(true) 在日志前添加时间戳
withFollow(boolean) 是否持续跟随(类似 -f

思考: 为什么日志要使用 Frame(帧)而不是纯文本?FrameType 的作用是什么?

Docker 容器的日志实际上包含两个数据流:标准输出(STDOUT)和标准错误(STDERR)。如果只用纯文本,无法区分一行日志到底来自哪个流。Frame 的 streamType 字段标记了数据来源,这样上层可以根据需求选择性处理(比如只收集错误日志用于告警)。

execCreateCmd + execStartCmd - 在容器中执行命令

在运行中的容器内部执行命令,对应 docker exec 命令。它分为两步

  1. execCreateCmd:创建 exec 实例,描述要执行什么命令
  2. execStartCmd:真正执行命令并接收输出

    /**
    * 在容器中执行命令(同步方式)
    * @param containerId 容器 ID
    * @param command 要执行的命令参数,如 ["ls", "-la"] 或 ["echo", "hello"]
    * @return 命令输出的文本
    */
    public String execCommandInContainer(String containerId, String... command) {
    StringBuilder output = new StringBuilder();
    try {
        // 1. 创建 exec 实例(描述要在容器中执行什么命令)
        ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                .withCmd(command)              // 要执行的命令
                .withAttachStdout(true)         // 挂接标准输出
                .withAttachStderr(true)         // 挂接错误输出
                .exec();
    
        // 2. 启动 exec(真正执行命令,并接收输出)
        dockerClient.execStartCmd(execCreateCmdResponse.getId())
                .exec(new ResultCallback.Adapter<>() {
                    @Override
                    public void onNext(Frame frame) {
                        if (frame.getStreamType() == FrameType.STDOUT ||
                                frame.getStreamType() == FrameType.STDERR) {
                            output.append(new String(frame.getPayload()));
                        }
                    }
                }).awaitCompletion();
        return output.toString();
    } catch (InterruptedException e) {
        log.error("执行容器命令被中断: {}", e.getMessage());
        Thread.currentThread().interrupt();
        return output.toString();
    }
    }
    

思考: 为什么 exec 要分为 create 和 start 两步?为什么不一步到位?

这个设计参考了 fork/exec 的 POSIX 模型:create 阶段创建新的进程上下文(包括环境变量、工作目录、附加流),start 阶段真正在目标容器内启动这个进程。分离的好处是可以在真正执行前检查命令是否合法、权限是否足够,失败时可以提前阻止而不产生任何影响。

waitContainerCmd - 等待容器退出

waitContainerCmd 用于阻塞当前线程,直到容器退出并返回退出码:

/**
 * 等待容器退出
 * @param containerId 容器 ID
 * @param timeoutSeconds 超时时间(秒)
 * @return 容器退出码(-1 表示超时)
 */
public int waitContainer(String containerId, int timeoutSeconds) {
    try {
        WaitContainerResultCallback callback = dockerClient.waitContainerCmd(containerId)
                .exec(new WaitContainerResultCallback());
        // 阻塞等待容器退出,设置超时
        int exitCode = callback.awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS);
        log.info("容器 {} 已退出,退出码: {}", containerId, exitCode);
        return exitCode;
    } catch (InterruptedException e) {
        log.error("等待容器退出被中断: {}", e.getMessage());
        Thread.currentThread().interrupt();
        return -1;
    }
}

这个 API 在 CI/CD 场景中非常有用:我们可以启动一个容器执行测试任务,然后等待它退出并检查退出码来判断测试是否通过。

运维完整示例

一个完整的运维操作流程如下:

public void opsDemo() {
    String containerName = "ops-demo-" + System.currentTimeMillis();
    try {
        // 1. 创建一个执行 echo 命令的容器
        String id = createTemporaryContainer(containerName, "sh", "-c", "echo HelloOps && sleep 999");
        log.info("1. 临时容器已创建并启动: {}", id);

        // 2. 在容器中执行命令
        String result = execCommandInContainer(id, "ls", "-la", "/");
        log.info("2. 容器内 ls -la / 输出:\n{}", result);

        // 3. 获取容器日志
        String logs = getContainerLogs(id, 10);
        log.info("3. 容器日志:\n{}", logs);

        // 4. 清理容器
        dockerClient.removeContainerCmd(id).withForce(true).exec();
        log.info("4. 临时容器已清理");
    } catch (Exception e) {
        log.error("运维演示失败: {}", e.getMessage());
    }
}

思考: 结合前面学习的 exec 与 logs 场景,在"容器内执行命令"获取到的输出与"查看容器日志"获取到的是否相同?它们有什么区别?

exec 获取的是在运行中的容器里新执行命令的输出,相当于 docker exec <container> <cmd>;而 logs 获取的是容器主进程(PID 1)启动以来产生的日志,相当于 docker logs <container>。两者针对的是不同数据源:exec 是临时命令的输出,logs 是容器自身的运行日志。


DockerClient 核心对象及 API 对照表

经过前面章节的学习,我们已经掌握了 Docker Java SDK 的主要使用方法。本章将对 DockerClient 接口的全部方法进行归类整理,形成一份速查表,方便日常开发时快速查找。

DockerClient 核心架构

DockerClient 是整个 SDK 的统一入口,所有 Docker 操作都通过它来分发。它的方法返回值是各种 *Cmd 接口,通过链式调用设置参数后,调用 .exec() 真正执行。

// DockerClient 方法的调用模式
dockerClient.<命令方法>(<参数>)
    .<参数方法>(<值>)      // 链式调用
    .<参数方法>(<值>)
    .exec();              // 真正执行,返回响应或回调

API 全局概览

以下表格列出了 DockerClient 接口的全部方法,按功能分类:

一、Docker Daemon 基本信息

方法 对应命令 说明
pingCmd() docker ping 检测 Docker daemon 连接是否正常
infoCmd() docker info 获取 Docker daemon 系统信息
versionCmd() docker version 获取 Docker 版本信息
authCmd() 进行 Registry 认证
eventsCmd() docker events 实时监听 Docker 事件

二、镜像管理

方法 对应命令 说明
listImagesCmd() docker images 列出本地所有镜像
inspectImageCmd(id) docker inspect (镜像) 获取镜像完整详情
pullImageCmd(name) docker pull 拉取镜像(异步回调)
pushImageCmd(name) docker push 推送镜像到 Registry
removeImageCmd(id) docker rmi 删除本地镜像
tagImageCmd(id, repo, tag) docker tag 为镜像添加标签
saveImageCmd(id) docker save 将镜像导出为 tar 流
saveImagesCmd() docker save (多个) 批量导出多个镜像
loadImageCmd(inputStream) docker load 从 tar 流加载镜像
searchImagesCmd(keyword) docker search 搜索 Registry 中的镜像
imageHistoryCmd(id) docker history 查看镜像构建历史
createImageCmd(name, stream) docker import 从 tar 流创建镜像

三、镜像构建

方法 对应命令 说明
buildImageCmd() docker build 从 Dockerfile 构建镜像
buildImageCmd(file) docker build 指定 Dockerfile 构建
buildImageCmd(inputStream) docker build 从 tar 包构建镜像

四、容器生命周期管理

方法 对应命令 说明
createContainerCmd(image) docker create 创建容器(不启动)
startContainerCmd(id) docker start 启动容器
stopContainerCmd(id) docker stop 优雅停止容器(先 SIGTERM,再 SIGKILL)
killContainerCmd(id) docker kill 强制杀死容器(立即 SIGKILL)
restartContainerCmd(id) docker restart 重启容器
pauseContainerCmd(id) docker pause 暂停容器(使用 cgroup freezer)
unpauseContainerCmd(id) docker unpause 恢复已暂停容器
removeContainerCmd(id) docker rm 删除容器
waitContainerCmd(id) docker wait 阻塞等待容器退出(返回退出码)
renameContainerCmd(id) docker rename 重命名容器
updateContainerCmd(id) docker update 更新容器运行时配置
commitCmd(id) docker commit 将容器状态保存为镜像
topContainerCmd(id) docker top 查看容器内运行的进程
exportContainerCmd(id) docker export 将容器文件系统导出为 tar

五、容器查询

方法 对应命令 说明
listContainersCmd() docker ps 列出容器
inspectContainerCmd(id) docker inspect (容器) 获取容器完整详情
containerDiffCmd(id) docker diff 查看容器文件系统变更
statsCmd(id) docker stats 获取容器资源统计

六、容器运维

方法 对应命令 说明
logContainerCmd(id) docker logs 获取容器日志
attachContainerCmd(id) docker attach 连接到容器的 I/O 流
execCreateCmd(id) docker exec (创建) 在容器内创建命令会话
execStartCmd(id) docker exec (启动) 执行创建好的命令会话
inspectExecCmd(id) 获取 exec 会话详情
resizeExecCmd(id) 调整 exec TTY 大小
copyArchiveFromContainerCmd(id, path) docker cp (取出) 从容器复制文件到本地
copyArchiveToContainerCmd(id) docker cp (放入) 将本地文件复制到容器
copyFileFromContainerCmd(id, path) docker cp (取出) 从容器复制单个文件
resizeContainerCmd(id) 调整容器 TTY 大小

七、数据卷(Volume)

方法 对应命令 说明
listVolumesCmd() docker volume ls 列出所有数据卷
inspectVolumeCmd(name) docker volume inspect 查看卷详情
createVolumeCmd() docker volume create 创建数据卷
removeVolumeCmd(name) docker volume rm 删除数据卷

八、网络(Network)

方法 对应命令 说明
listNetworksCmd() docker network ls 列出所有网络
inspectNetworkCmd() docker network inspect 查看网络详情
createNetworkCmd() docker network create 创建自定义网络
removeNetworkCmd(id) docker network rm 删除网络
connectToNetworkCmd() docker network connect 将容器接入网络
disconnectFromNetworkCmd() docker network disconnect 将容器移出网络

核心模型类对照

SDK 中的模型类与 Docker JSON API 的返回数据一一对应。以下是常用的模型类:

Image 相关

模型类 用途 常用字段
Image 镜像简要信息 id, repoTags, size, created, labels
InspectImageResponse 镜像完整详情 id, arch, os, config, rootFs
ImageHistory 镜像构建历史 created, createdBy, size

Container 相关

模型类 用途 常用字段
Container 容器简要信息 id, names, state, status, ports
InspectContainerResponse 容器完整详情 id, name, state, config, networkSettings, mounts
ContainerState 容器状态 running, status, exitCode, pid
ContainerPort 端口映射 privatePort, publicPort, type
Mount 挂载点 source, destination, mode, type

命令响应

模型类 用途 常用字段
CreateContainerResponse 创建容器响应 id, warnings
ExecCreateCmdResponse 创建 exec 响应 id
Version 版本信息 version, apiVersion, buildTime
Info 系统信息 containers, images, serverVersion

回调类与异步机制

Docker SDK 中的 I/O 密集操作(拉取镜像、构建镜像、执行命令等)采用异步回调模式。核心回调类:

回调类 用途 常用方法
ResultCallback.Adapter<T> 通用异步回调适配器 onNext(T), onComplete(), onError(Throwable)
PullImageResultCallback 拉取镜像回调 awaitCompletion()
BuildImageResultCallback 构建镜像回调 awaitImageId()
WaitContainerResultCallback 等待容器回调 awaitStatusCode(timeout, unit)
LogContainerResultCallback 获取日志回调 继承自 ResultCallback,逐帧回调
ExecStartResultCallback 执行命令回调 继承自 ResultCallback,逐帧回调
LoadImageResultCallback 加载镜像回调 awaitCompletion()

错误处理机制

SDK 中的所有 Docker 操作失败时,统一抛出 DockerException 的子类:

异常类 HTTP 状态码 说明
NotFoundException 404 资源不存在(镜像/容器未找到)
ConflictException 409 资源冲突(如删除运行中的容器)
InternalServerErrorException 500 Docker daemon 内部错误
BadRequestException 400 请求参数不正确
ForbiddenException 403 权限不足(Docker socket 权限或 Registry 认证失败)

最佳实践:在调用 Docker API 时,始终用 try-catch 捕获异常并进行适当的错误处理:

try {
    dockerClient.pingCmd().exec();
    log.info("Docker 连接正常");
} catch (DockerException e) {
    // 根据异常类型进行针对性处理
    if (e instanceof NotFoundException) {
        log.error("请求的资源不存在");
    } else if (e instanceof InternalServerErrorException) {
        log.error("Docker daemon 内部错误: {}", e.getMessage());
    } else {
        log.error("Docker 操作失败: {}", e.getMessage());
    }
} catch (Exception e) {
    log.error("网络连接失败: {}", e.getMessage());
}

本教程代码结构

类名 所在章节 说明
QuickStart Chapter 2 DockerClient 创建、连接、拉取镜像、创建启动容器
DockerClientFactory Chapter 2 DockerClient 工厂类,提供统一的实例创建方法
ImageManageAPI Chapter 3 镜像管理(列出、查看详情、拉取、删除、标记、检查存在)
ImageBuildAPI Chapter 4 镜像构建(从 Dockerfile 构建、清理中间层)
ContainerManageAPI Chapter 5 容器管理(创建、启动、停止、删除、列出、查看详情)
ContainerOpsAPI Chapter 6 容器运维(日志、执行命令、复制文件、等待退出)

Docker Java SDK 的 API 设计遵循"命令对象模式":每个操作对应一个 *Cmd 接口,通过 Builder 模式链式设置参数,最后通过 .exec() 统一执行。掌握这个模式后,即使遇到本教程未覆盖的 API,也能快速上手。