首页 > 编程笔记

Python docker-py模块的用法

我们希望和 Docker 服务器对话,但并不打算按照 Remote API 的格式给其发送消息,因为这有点麻烦。这种操作方式需要构造特定的消息,然后发送给服务器,再将服务器的消息进行解析。

此时可以使用 docker-py 库帮我们做这些繁杂的工作,我们只需要使用类似于 Docker 客户端的格式给 docker-py 发送命令即可。

docker-py 是一个第三方库,所以在使用之前需要自行安装。推荐使用 PIP 进行安装,命令如下:

pip install docker-py

目前的docker-py版本是1.10.6。安装完成后可以运行下面的代码查看是否安装成功。
>>> import docker                # 引入docker模块
>>> docker.version_info            # 查看版本信息
(1, 10, 6)                        # 当前版本是1.10.6
>>> docker.version                # 另外一种查看版本信息的方法
'1.10.6'
由于我们的主要操作是针对 Docker 服务器的,所以我们编写的代码基本上都算作是客户端。在 docker-py 中最重要的操作基本上都是在 Docker.client 模块中,其提供了对 Docker 引擎的相关操作。

建立连接

Docker 服务器对外提供的是 Web 接口,就是接收 HTTP 消息并返回 HTTP 回应。但本教程并不打算介绍这些底层的消息格式,也不打算直接给 Docker 服务器发送这些 HTTP 消息,而是使用客户端的高层接口函数来解决这个问题。这些接口函数相对比较直观,使用也更方便。而且基本和 Docker 命令行客户端的用法类似。

在对 Docker 服务器进行操作之前,首先需要建立连接,这时可以使用 docker.from_env() 接口函数来建立连接。多数情况下不需要传输参数就可以正确建立连接,方法如下:

client = docker.from_env()

另外一个建立连接的方式是构造一个 Client 对象,该对象的初始化函数接收下面的参数:
下面的代码演示了如何和 Docker 服务器建立连接;如何在建立连接后得到服务器的版本信息;如何得到服务器支持的接口信息以及如何获取服务器的编译时间信息。
>>> import docker
>>> client = docker.Client()
>>> if client is None:
...     print("connection failed")
... else:
...     print("connection done")
...                                         # 结束if语句
connection done                            # 连接成功
>>> client.base_url                        # 默认的连接方式
'http+docker://localunixsocket'
>>> client.timeout                        # 默认的连接超时时间是60秒
60
>>> ver_info = client.version()        # 得到服务器的信息
>>> ver_info['Version']                    # 服务器的版本信息
'18.06.1-ce'   
>>> ver_info['ApiVersion']                # API版本信息
'1.38'
>>> ver_info['KernelVersion']
'4.9.93-linuxkit-aufs'
>>> ver_info['BuildTime']                # 编译时间信息
'2018-08-21T17:29:02.000000000+00:00'

对Docker镜像的操作

Docker 的镜像就像是软件的安装包。对其操作主要有下载安装包、删除安装包等。对应到 Docker 镜像就是 pull 和 rmi(remove image) 操作。

如果想下载某个镜像,默认命令是“docker pull 镜像名”。例如希望下载版本是 3.9 的 al phine 镜像,可以使用下面的命令:

$ docker pull alpine:3.9

类似地,我们用代码实现该功能如下:
import docker
client = docker.from_env()
imgs1 = client.images()                    # 列出所有本地的镜像
client.pull("alpine")                    # 得到所有的alpine镜像
imgs2 = client.images()                    # 再次查看所有的本地镜像
for img in imgs2:
    if img not in imgs1:
        print(img)
上面的代码会 pull 所有的 alpine 镜像。如果运行上面的代码,可以看到其下载到本地的镜像数目庞大。
$ docker images                            # 查看所有的镜像
REPOSITORY        TAG          IMAGE ID         CREATED            SIZE
alpine             3.9             055936d39205        5 weeks ago          5.53MB
alpine              3.9.4           055936d39205        5 weeks ago          5.53MB
alpine              latest          055936d39205        5 weeks ago          5.53MB
alpine               20190508        43cffc6f84a4        5 weeks ago          5.56MB
alpine             edge            43cffc6f84a4        5 weeks ago          5.56MB
......

通常这不是我们希望看到的结果,毕竟镜像体积比较大,这样会花费很多的时间,对硬盘也是一种浪费,所以我们一般会指定 tag 信息。例如,只下载 tag 为 3.9 的版本,那么可以用下面的代码。
import docker
client = docker.from_env()
imgs1 = client.images()
client.pull("alpine:3.9")
imgs2 = client.images()
for img in imgs2:
    if img not in imgs1:
        print(img)
如果希望看到下载镜像时的过程,那么在执行 pull 命令时使用参数 stream=True,这时就会看到下载的全部过程。
import docker
client = docker.from_env()
out_stream = client.pull("alpine:3.9", stream=True)
for line in out_stream:
    print(line)
运行上面的程序,可以看到下面的输出:

$ python pullImageWithTagAsStream.py
b'{"status":"Pulling from library/alpine","id":"3.9"}\r\n'
b'{"status":"Pulling fs layer","progressDetail":{},"id":"e7c96db718
     1b"}\r\n'
b'{"status":"Downloading","progressDetail":{"current":27999,"total":
     2757034},"
......

可以看到这些进度的消息,并可以将这些消息显示在 GUI 界面上,给用户提示下载镜像的过程,让用户看到下载的进度。

删除镜像也是基本的操作。删除镜像需要知道镜像的 ID,但多数情况下我们只知道镜像的名字和标签。所以需要得到所有的镜像,并从中找到要删除的镜像 ID,然后再将其删除。

这里需要注意的是,一个镜像可能有多个标签(tag),例如某个镜像可以有 1.2.1、1.2、1 和 latest 四个标签,所以镜像的 RepoTags 属性是一个列表。遍历这个列表,查看是否有要找的标签。
import docker                                # 这个必须有,引入Docker包
client = docker.from_env()                    # 和Docker服务器建立连接
imgs = client.images()                        # 得到Docker镜像列表
for img in imgs:                            # 遍历所有的镜像
        # 一个镜像可能有几个tag,只要有一个符合要求即可
    for tag in img['RepoTags']:
        if tag == "alpine:3.9":                # tag是我们要找的
            client.remove_image(img['Id'])    # 删除挑选出来的镜像
运行该脚本,输出如下:
$ docker images                                # 查看当前的镜像状态
REPOSITORY   TAG    IMAGE ID     CREATED      SIZE
alpine            3.9      055936d39205  2 months ago   5.53MB
$ python delImageByTag.py                    # 执行脚本删除之
$ docker images                            # 再次查看镜像状态,已经没有镜像了
REPOSITORY   TAG    IMAGE ID     CREATED      SIZE
如果某个镜像正在运行中,运行上面的脚本就会出错,例如下面的情况:
$ docker run -d alpine:3.9                 # 使用镜像启动一个容器
# 本地没有该镜像,先从服务器下载下来
Unable to find image 'alpine:3.9' locally
3.9: Pulling from library/alpine
e7c96db7181b: Pull complete
Digest: sha256:7746df395af22f04212cd25a92c1
d6dbc5a06a0ca9579a229ef43008d4d1302a
Status: Downloaded newer image for alpine:3.9    # 镜像下载成功
3cfcf7b1b8268ac576572245caf3248c023b9680e5c3f5d666cc5a6c71d0819f
$ python delImageByTag.py                    # 执行删除镜像的脚本
Traceback (most recent call last):            # 抛出HTTPError异常
......
为了解决这个问题,可以使用强制删除的方式。方法就是带上参数 force=True。其作用类似于 docker rmi-f xxxx 命令。下面是修改后的代码:
import docker                            # 必须要引入的Docker包
client = docker.from_env()                # 和Docker服务器建立连接   
imgs = client.images()                    # 得到所有的本地镜像
for img in imgs:
    for tag in img['RepoTags']:
        if tag == "alpine:3.9":             # 有我们要找的标签
            client.remove_image(img['Id'], force=True)    # 强制删除

如果希望删除所有的镜像,则还是先得到所有的镜像文件信息,然后将这些文件逐个删除。下面的代码演示了这个过程。
import docker                            # 必须要引入的Docker包
client = docker.from_env()                # 和Docker服务器建立连接
imgs = client.images()                    # 得到所有的本地镜像
for img in imgs:                        # 依次遍历镜像文件
    client.remove_image(img['Id'])        # 删除镜像文件

如果有一个自己的 Dockerfile,内容如下:

FROM busybox:latest
CMD ["/bin/sh"]

如果希望使用这个 Dockerfile 来创建自己的容器镜像,可以使用下面的代码:
import docker
client = docker.from_env()
# 注意,一定要以二进制方式打开
fd = open("./Dockerfile", "rb")
result = client.build(fileobj=fd, rm=True, tag='pydocker/demo')
fd.close()
for line in result:
    print(line)
该脚本的执行情况如下:

$ python buildImg.py
b'{"stream":"Step 1/2 : FROM busybox:latest"}\r\n{"stream":"\\n"}
     \r\n'
b'{"stream":" ---\\u003e e4db68de4ff2\\n"}\r\n{"stream":"Step 2/2 :
CMD [\\"/bin/sh\\"]"}\r\n{"stream":"\\n"}\r\n'
b'{"stream":" ---\\u003e Using cache\\n"}\r\n{"stream":"
---\\u003e 266fb6ebcc54\\n"}\r\n{"stream":"Successfully built
     266fb6ebcc54\\n"}\r\n'
b'{"stream":"Successfully tagged pydocker/demo:latest\\n"}\r\n'
$ docker run -d pydocker/demo
d42108796ec0fd6d899d282bab91be455237b7cd45e0df29762d272cf0552e3b


如果需要推送镜像文件到服务器,这时一般需要先登录,然后再进行推送操作。下面是一个从登录到推送的例子。
import docker
client = docker.from_env()
# 指定用户名、密码和服务器
login_ret = client.login(username="lovepython",     # 用户名
                    password="py",                     # 密码
                    registry="repo.docker.com")        # 服务器信息
print("login_ret:", login_ret)
# 将指定的镜像推送到服务器上
push_rsp = client.push("pydocker/demo", stream=False)
print(“push_rsp:”, push_rsp)
运行该脚本,得到下面的输出:

# 登录成功
login_ret: {'IdentityToken': '', 'Status': 'Login Succeeded'}
push_rsp: {"status":"Pushing repository pydocker/demo (1 tags)"}\\n',
'{"status":"Pushing","progressDetail":{},"id":"511136ea3c5a"}\\n',
'{"status":"Image already pushed, skipping","progressDetail":{},
    "id":"511136ea3c5a"}\\n',
...
'{"status":"Pushing tag for rev [918af568e6e5] on {
    https://repo.docker.com/v1/repositories/
    pydocker/demo/app/tags/latest}"}

这里需要注意的是第 2 行的 login() 返回值,状态 Status 表示成功。如果不成功,就不会继续进行下面的推送操作。

运行Docker镜像

启动 Dockder 镜像需要设定一些参数,如开放端口号、文件映射等,所以运行镜像的代码也会比较复杂。下面是一个简单的例子。
import docker
import time
ports = [80]                            # 暴露出来的Docker内部端口
# Docker的内部端口是80,Docker的外部端口是9999
port_bindings = {80: 9999}
volumes = ['/container']
volume_bindings = {
    '/lovepython/runImg.py': {
        'bind': '/runImg.py',
        'mode': 'rw',
    },
}
client = docker.from_env()
host_config = client.create_host_config(
    binds=volume_bindings,
port_bindings=port_bindings,
)
#下载镜像到本地,必须先行下载对应的镜像
client.pull("nginx:latest")
container = client.create_container(
image='nginx',
     ports=ports,
     volumes=volumes,
host_config=host_config,
)
client.start(container)
container_id = container['Id']
print("Container ID:", container_id)
time.sleep(3)
client.pause(container_id)            # 暂停容器运行
docker_inst_tmp = client.inspect_container(container_id)
print(docker_inst_tmp['State']['Status'])
time.sleep(3)
client.unpause(container_id)        # 恢复容器运行
docker_inst_tmp = client.inspect_container(container_id)
运行该脚本,可以看到下面的输出:

$ python runImg.py
2f19d3e39219097814c7aef957043d9605bca74c2cef290a54006008dfefb8b4
$ docker ps -q                        # 查看在运行的容器实例
2f19d3e39219                        # 这就是我们的脚本启动起来的容器实例

645
$ python runImg.py
2f19d3e39219097814c7aef957043d9605bca74c2cef290a54006008dfefb8b4
$ docker ps -q                        # 查看在运行的容器实例
2f19d3e39219                        # 这就是我们的脚本启动起来的容器实例


如果希望看到所有的容器,就是类似于 docker ps 这样的功能,那么可以使用 containers() 接口,该接口返回一个容器列表。下面是一个简单的例子。
import docker
client = docker.from_env()
container_list = client.containers()        # 得到所有的容器
for container_inst in container_list:
    print(container_inst['Id'])                # 显示这些容器的ID
运行后的输出如下:

$ python docker_ps.py
2f19d3e39219097814c7aef957043d9605bca74c2cef290a54006008dfefb8b4


containers() 接口有很多参数,如 quiet 参数表示是否只得到容器 ID,默认该参数为 False,如果设为 True,那么输出的只是一个比较短的容器 ID;all 参数用来控制是否返回所有的容器,包括以已经退出但是还没有删除的容器实例,该参数默认值是 False。

如果要杀掉该容器,则可以使用接口 kill(容器ID)。下面是使用该接口的一个例子。
import docker
client = docker.from_env()
container_list = client.containers()
for container_inst in container_list:
    if container_inst['Image'] == 'nginx':    # 找到我们要删除的容器
        client.kill(container_inst['Id'])    # 删除该容器

kill()接口还有一个参数 signal,表示给容器进程发送什么信号让其退出,默认值是 SIGKILL。该参数可以是字符串类型,也可以是整数类型。

如果希望暂停然后恢复某个 Docker 的运行,则可以使用接口 pause(容器ID)和 unpause(容器ID)来达到目的。下面是一个例子,在该例子中首先启动一个容器,然后暂停该容器,再恢复运行,最后杀掉该容器。下面是完整的代码。
import docker
import time
ports = [80]                        # 暴露出来的Docker内部的端口
# Docker内部的端口是80,Docker外部的端口是9999
port_bindings = {80: 9999}
volumes = ['/container']
volume_bindings = {
    '/lovepython/runImg.py': {
        'bind': '/runImg.py',
        'mode': 'rw',
    },
}
client = docker.from_env()
host_config = client.create_host_config(
    binds=volume_bindings,
    port_bindings=port_bindings,
)
# 下载镜像下来,必须先行下载镜像
client.pull("nginx:latest")
container = client.create_container(
     image='nginx',
     ports=ports,
     volumes=volumes,
     host_config=host_config,
)
client.start(container)
container_id = container['Id']
print("Container ID:", container_id)
time.sleep(3)
client.pause(container_id)            # 暂停容器运行
docker_inst_tmp = client.inspect_container(container_id)
print(docker_inst_tmp['State']['Status'])
time.sleep(3)
client.unpause(container_id)        # 恢复容器运行
docker_inst_tmp = client.inspect_container(container_id)
print(docker_inst_tmp['State']['Status'])
client.kill(container_id)            # 杀掉容器
client.wait(container_id)            # 等待容器被杀掉
运行后的输出结果如下:

$ python docker_pause_resume.py
Container ID:
468e0965c8fd7bf7d575c4a5c401f98d5bf4bdf6b9ed7047c8a0de224a1b992c
paused                                # 第32行的输出
running                                # 第36行的输出

优秀文章