# 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); ``` *** 下一章,我们将学习如何使用 Docker Java SDK 从 Dockerfile 构建自定义镜像。