# 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` 用户组中:
```bash
# 将当前用户添加到 docker 用户组
sudo usermod -aG docker $USER
# 查看当前用户所属 groups
groups
# 如果 usermod 不生效,可以尝试 newgrp 刷新组信息
newgrp docker
```
添加完成后,需要**重新登录系统**或**重启系统**让修改生效。我们可以通过以下命令验证是否配置成功:
```bash
# 查看 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 socket** 和 **TCP**。
#### 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` 中添加配置:
```json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
```
**方法二:修改 systemd service 配置**
创建或编辑 systemd 配置文件:
```bash
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vi /etc/systemd/system/docker.service.d/override.conf
```
添加以下内容:
```ini
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock
```
然后重启 Docker 服务:
```bash
sudo systemctl daemon-reload
sudo systemctl restart docker
```
验证 TCP 监听是否生效:
```bash
# 查看 Docker daemon 监听的端口
ss -tlnp | grep 2375
# 或使用 curl 测试
curl http://localhost:2375/version
```
**注意:** 开启 TCP 监听后,任何能够访问该端口的客户端都可以操作 Docker daemon,存在严重的安全风险。在生产环境中,建议配合 TLS 证书进行加密通信。
#### TLS 安全通信配置
在生产环境中使用 TCP 协议时,应该启用 TLS 加密:
```bash
# 创建 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` 中添加以下配置:
```xml
com.github.docker-java
docker-java
3.7.1
com.github.docker-java
docker-java-transport-httpclient5
3.7.1
ch.qos.logback
logback-classic
1.5.38
```
这里我们需要两个核心依赖:`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
镜像(Image)是 Docker 的核心概念之一,它是一个只读的模板,包含了运行应用所需的所有内容:代码、运行时、系统工具、库和配置。Docker Java SDK 提供了一系列镜像管理 API,本章我们来逐一学习。
### listImagesCmd - 列出所有镜像
`listImagesCmd()` 方法用于列出 Docker daemon 上的镜像列表,对应 Docker CLI 的 `docker images` 命令。
```java
/**
* 列出自定义节点上所有存在的镜像
* @return 镜像列表
*/
public List listImages() {
List 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()` 方法:
```java
/**
* 获取镜像的详细信息
* 包括镜像的 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` 命令。与前面几个方法不同,拉取镜像是一个**异步操作**,需要借助回调机制来处理结果:
```java
/**
* 拉取指定名称的镜像
* @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` 命令:
```java
/**
* 删除本地镜像
* @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` 命令,常用于给镜像重命名或在本地建立镜像副本:
```java
/**
* 为镜像添加标签(相当于 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`:
```java
api.tagImage("nginx:latest", "my-nginx", "v1.0");
```
### saveImageCmd / loadImageCmd - 导入导出镜像
Docker 还支持将镜像导出为 tar 文件,或者从 tar 文件导入镜像。这在离线部署场景中非常有用:
```java
// 将镜像导出为 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 盘或内网传输到目标机器上加载。这种方式不依赖网络连接,但文件传输本身需要额外的存储空间。
### 镜像管理完整示例
```java
// 列出所有镜像
List 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 和构建所需的文件)。
```java
/**
* 从 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)
```
每次 `RUN`、`COPY` 等指令都会创建一个新的镜像 Layer,所有 Layer 叠加在一起就是最终的镜像。这个分层机制使得镜像可以被高效地缓存和复用。
### 示例:构建一个带标识文件的自定义镜像
假设我们有如下 Dockerfile:
```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` 构建并验证:
```java
ImageBuildAPI api = new ImageBuildAPI();
String imageId = api.buildImage("src/test/resources/Dockerfile", "sdk-demo/hello:latest");
log.info("构建结果: {}", imageId);
// 验证镜像已存在
List 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 - 创建容器
```java
/**
* 创建容器(不启动)
* @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)` | 设置资源限制、挂载卷、网络模式等 |
创建带环境变量和端口映射的容器:
```java
// 创建带环境变量的容器
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()` 启动它:
```java
/**
* 启动容器
* @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` 强制终止:
```java
/**
* 停止容器(等待容器优雅退出)
* @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` 信号,进程立即终止,不给优雅退出的机会:
```java
/**
* 强制停止容器(立即发送 SIGKILL)
* @param containerId 容器 ID
*/
public void killContainer(String containerId) {
dockerClient.killContainerCmd(containerId).exec();
log.info("容器 {} 已被强制停止", containerId);
}
```
**思考:** 什么场景下应该用 stop,什么场景下应该用 kill?
`stop` 会先给进程发 `SIGTERM`,让应用有机会保存数据、释放资源(比如数据库需要执行清理操作);`kill` 则是立即杀死进程,可能导致数据丢失。生产环境中应该优先使用 `stop`,只有在进程无法响应或严格限时的情况下才使用 `kill`。
### removeContainerCmd - 删除容器
删除容器时需要考虑两个选项:
```java
/**
* 删除容器
* @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)` 则显示所有容器(包括已停止的):
```java
/**
* 列出所有容器(包括停止的)
* @return 容器列表
*/
public List listContainers() {
// withShowAll(true) 展示所有容器(不只运行中的)
List 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 - 获取容器详细信息
与镜像类似,容器也有详查方法。它返回容器的完整配置、网络、挂载信息:
```java
/**
* 获取容器详细信息
* @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()` | 创建时间 |
### 完整生命周期示例
```java
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);
}
```
**思考:** 查看 `Container` 的 `getNames()` 返回的数组——为什么是一个数组而不是单个字符串?
Docker 的设计中,一个容器可以有多个名称(别名),通常是 `/容器名`。当使用 `--link` 方式连接容器时,会为被连接的容器创建额外的名称。虽然现代 Docker 更推荐使用自定义网络,但 `getNames()` 的设计保留了这种能力。
***
## 容器运维相关API
前面我们学习了容器的基础管理(创建、启动、停止、删除),本章将深入讲解容器的**运维操作**:查看日志、在容器中执行命令、文件复制、等待容器退出等。这些操作是日常运维中最常用的功能。
### logsCmd - 获取容器日志
获取容器日志对应 `docker logs` 命令。日志以**帧(Frame)**的方式流式返回,每帧包含数据类型和载荷:
```java
/**
* 获取容器日志(同步方式)
* @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`:真正执行命令并接收输出
```java
/**
* 在容器中执行命令(同步方式)
* @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` 用于阻塞当前线程,直到容器退出并返回退出码:
```java
/**
* 等待容器退出
* @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 场景中非常有用:我们可以启动一个容器执行测试任务,然后等待它退出并检查退出码来判断测试是否通过。
### 运维完整示例
一个完整的运维操作流程如下:
```java
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 `;而 `logs` 获取的是**容器主进程**(PID 1)启动以来产生的日志,相当于 `docker logs `。两者针对的是不同数据源:exec 是临时命令的输出,logs 是容器自身的运行日志。
***
## DockerClient 核心对象及 API 对照表
经过前面章节的学习,我们已经掌握了 Docker Java SDK 的主要使用方法。本章将对 `DockerClient` 接口的全部方法进行归类整理,形成一份**速查表**,方便日常开发时快速查找。
### DockerClient 核心架构
`DockerClient` 是整个 SDK 的统一入口,所有 Docker 操作都通过它来分发。它的方法返回值是各种 `*Cmd` 接口,通过链式调用设置参数后,调用 `.exec()` 真正执行。
```java
// 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` | 通用异步回调适配器 | `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 捕获异常并进行适当的错误处理:
```java
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,也能快速上手。