Solomon Hykes — Docker 创始人
Docker 提供了一个核心的抽象层,让你可以在任何基础设施上运行任何应用。这种抽象层的本质是:将应用的运行环境打包成一个标准化的单元,这个单元可以在任何地方运行。
那么,从这章开始,就让我们来感受一下,Docker Java SDK 为我们带来了什么。
在使用 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 socket 和 TCP。
Unix socket 是默认的连接方式,适用于 Docker daemon 运行在本机的场景。SDK 通过以下地址连接:
unix:///var/run/docker.sock
这种方式的优点是无需额外配置,安全性高(通过文件权限控制访问),但缺点是只能连接本机的 Docker daemon。
当需要远程连接 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 证书进行加密通信。
在生产环境中使用 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、拉取镜像,以及创建并启动一个容器。
首先,我们需要在项目中引入 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 是 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);
});
}
}
自定义配置的创建分为三个步骤:
DockerClientConfig:配置 Docker daemon 地址、TLS、Registry 认证等DockerHttpClient:配置传输层参数,如连接数、超时时间等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,包括列出镜像、查看详情、删除镜像等操作。