0%

Docker-Java 使用小记

  • 前置知识
    • Java 语言
    • Springboot 框架
    • docker 基础知识
    • dockerfile 的使用

作者在最近项目中用 docker-java 动态部署 docker 。总结了一些经验。

0x01 依赖项

0x01x01 Maven

正常使用就两个依赖项,官方文档有写。

版本号以 maven 官方仓库的最新版本为准。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.3.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.3.3</version>
</dependency>

0x01x02 java.lang.NoClassDefFoundError

Maven 配置不当 / 抽风的情况下, 偶尔 docker-java 本身的依赖不会自动加载进来。导致调用后续使用中马上会报错。

1
2
java.lang.NoClassDefFoundError
java.lang.ClassNotfoundException

这两者有一定区别,不过这里是同时报出来的。看堆栈的话 NoClassDefFoundError 在先。

自行查看报错堆栈,导入没有的包即可。

作者当时是少了这个:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>5.2.2</version>
</dependency>

比较坑,因为 httpcore5 是存在的,这里报找不到类导致作者一直在乱找问题,翻了好久才发现其下没有 http2 的类,要导入 httpcore-h2 才好。

0x02 初始化

首先要连接到本地的 docker 服务上。

0x02x01 docker-java.properties

Docker-java 官方文档中介绍了多种方式修改 docker 的默认属性。

使用配置文件方式的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -docker version 查看 API version
api.version=1.43

# 2375 端口,不可暴露。
DOCKER_HOST=tcp://localhost:2375

# 2376 端口,支持开启 TLS
#DOCKER_HOST=tcp://localhost:2376

# 关闭tls
DOCKER_TLS_VERIFY=0

# TLS 证书
#DOCKER_CERT_PATH=/home/user/.docker/certs
#DOCKER_CONFIG=/home/user/.docker

# 私有 registry 搭建,镜像不是很多可以不用
#registry.url=http://url:5000/v2/
#registry.username=QST
#registry.password=QinShutian
#registry.email=qst@ckw.com

项目上线时 tls 这部分可能很重要, docker 服务本身没有身份验证机制,公网上 2375 端口暴露会使黑客甚至任何人都能够恶意操作 docker 。

故而如果不可避免地要暴露 docker 服务,不能使用 2375 端口。

目前作者的项目体量小,服务全在本地,封闭 2375 不让外界访问即可,故此处不展开。

0x02x02 设置 DockerClient

基本使用官方文档中的示例即可。

如果使用 docker-java.properties , createDefaultConfigBuilder() 创建的默认 config 中,属性即为你配置文件中所写的属性。如果代码中需要改动,也可以在 build 前使用 with 方法注入。

1
2
3
4
5
6
7
8
9
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);

DockerClient 创建完成后,接下来就可以相对优雅地调用 dockerAPI 了。

0x03 操作 docker (基于 dockerfile )

docker-java 提供的方法其实非常全面,基本建立了 dockerClient 就能涵盖对 docker 服务的所有操作。这里拣最基本最重要的介绍一下。

作者主要用到的是自定义镜像和容器管理,未涉及镜像仓库管理( pull&push )。

0x03x01 dockerClient 用法

dockerClient 中方法的使用基本都一致。

1
2
XxxxxxCmd cmd = dockerClient.xxxxxxCmd().withxxx().withxxx(); // 创建命令
cmd.exec() // 执行命令(同步)

.exec() 方法这里分为同步的和异步的。

开发者大概是考虑到部分 docker 操作工程量比较大,耗时长,故把这些操作的原型统一为异步抽象类。与同步操作的区别就是这里异步操作的 .exec() 方法需要指定回调。

翻了一下源码,异步操作比较少,但有几个还是很常用的。

![23-10-21-Docker-Java 使用 1](.._pics\23-10-21-Docker-Java 使用 1.png)

异步操作的 .exec() 方法是有参的。

1
.exec(callback)

故而要先实例化一个对应的 callback 对象。

1
XxxxxxCallback callback = new XxxxxxCallback();

也可以重写函数体输出一些调试信息之类的,当然正常使用也行。

1
2
3
4
5
6
XxxxxxResultCallback callback = new XxxxxxResultCallback(){
@override
public void onNext(XxxxResponseItem item) {
// your action
}
};

0x03x02 创建镜像

1
2
3
4
String imageName; // 镜像名
String dockerfilePath; // dockerfile 路径
File dockerfile = new File(dockerfilePath); //dockerfile 对象
String containerName; // 容器名

docker-java 没有直接调用 docker run 的接口 ,这是因为 run 本质上就是 build + create 两步操作依次进行。故而这里首先介绍一下 build 。

示例:

1
2
3
4
BuildImageResultCallback callback=new BuildImageResultCallback(); // 创建 buildImage 的回调
// 创建镜像
dockerClient.buildImageCmd(dockerfile).withTag("local/" + imageName).exec(callback).awaitImageId(); // await阻塞,等待异步线程执行完(相当于)
log.info("Image built. => " + dockerClient.listImagesCmd().exec().get(0));

这里 withTag(String tag) 会报过时,代码建议是换用withTags(Set<String> tags) (因为 docker 一个镜像可以打很多 tag )。想用就用,影响不大。

0x03x03 创建容器

示例:

1
2
3
4
// 创建容器
dockerClient.createContainerCmd("").withName(containerName).withEnv("ENVxxx=xxx")
.withPortBindings(PortBinding.parse("80:")).withImage("local/" + imageName).exec();
log.info("Container created. => " + dockerClient.listContainersCmd().withShowAll(true).exec().get(0));

参数不一一解释了,挺直观的。

0x03x04 启动容器

示例:

1
2
3
4
5
6
7
8
// 启动容器
dockerClient.startContainerCmd(containerName).exec();
try {
Thread.sleep(500);
} catch (Exception ignoredE) {} // 这里为什么要 sleep ?
// 获取容器信息
Container container = dockerClient.listContainersCmd().withShowAll(true).exec().get(0);
log.info("Container started. => " + container);

*** 什么获取容器信息之前要 sleep ? ***

0x03x05 获取端口映射

注意到创建容器的时候建立了一个端口映射:

.withPortBindings(PortBinding.parse(“80:”))

意为将容器的 80 端口映射到宿主机的随机端口。

端口映射:

1
2
80:       #将容器 80 端口映射到宿主机随机(高阶)端口
80:8080 #将容器 80 端口映射到宿主机指定端口(8080)

由于随机端口映射是在容器启动时确定的,故而容器启动后才能 get 到。

回到上一小节启动的容器:

1
Container container = dockerClient.listContainersCmd().withShowAll(true).exec().get(0);

可以直接从 container 中获得端口信息:

1
Integer port = container.getPorts()[0].getPublicPort();

回到刚才的问题,启动容器和获取信息过程之间为什么要 sleep ?

经过我的测试,刚启动的 container 的端口映射是不稳定的,需要经历很短时间的分发过程。当你在创建完端口之后立即得到 Container 对象,其端口信息与后来在 docker 服务里看到的端口信息不符。故而要 sleep ,等待端口映射稳定。如此这般。

0x03x06 关闭&删除容器

示例:

1
2
3
4
5
6
// 关闭容器
dockerClient.killContainerCmd(containerName).exec();
log.info("Container killed. => " + containerName);
// 删除容器
dockerClient.removeContainerCmd(containerName).exec();
log.info("Container removed. => " + containerName);

0x03x07 其他操作

pull、push 等,其调用大同小异,甚至不用翻源码,看代码提示即可。命令构造器构造命令,根据需求适当通过 withXxx 方法加参数,最后调用 .exec() 方法执行之。(异步方法有参,传入实例化的对应 XxxxxxCallback )

0x04 总结

大概这些。后面要开一下 TLS 。