关于Docker,每个数据科学家都应该知道什么?
什么是Docker?
想象一下,你是空间站上的一名宇航员,打算到外面去欣赏风景。你会面临恶劣的条件。温度、氧气和辐射都不是你的本职工作。人类需要一个特定的环境来茁壮成长。为了在任何其他情况下正常运作,如在海洋深处或高处的太空,我们需要一个系统来重现这种环境。无论是宇航服还是潜水艇,我们都需要隔离和确保我们所依赖的氧气、压力和温度水平的东西。
换句话说,我们需要一个容器。
任何软件都面临着与宇航员相同的问题。只要我们离开家,到外面的世界去,环境就会变得很恶劣,一个保护机制来重现我们的自然环境是必须的。Docker容器就是程序的太空服。
Docker将软件与同一系统中的所有其他事物隔离开来。在 "宇航服 "内运行的程序通常不知道自己穿的是宇航服,并且不受外面发生的任何事情影响。
容器化堆栈
- 应用。高级别的应用(你的数据科学项目)
- 依赖性。低级别的通用软件(如Tensorflow或Python)。
- Docker容器。隔离层
- 操作系统。低级别的接口和驱动,与硬件互动
- 硬件CPU、内存、硬盘、网络,等等。
其基本思想是将一个应用程序及其依赖关系打包成一个可重复使用的工件,它可以在不同的环境中可靠地实例化。
如何创建一个容器?
创建Docker容器的流程。
- Dockerfile编译镜像的说明
- 图像编译后的人工制品
- 容器镜像的一个执行实例
Docker文件
首先,我们需要指令。
我们可以定义宇航服的温度、辐射和氧气水平,但我们需要指令,而不是需求。Docker是基于指令的,而不是基于要求的。我们将描述如何而不是什么。为了做到这一点,我们创建一个文本文件,并将其命名为Dockerfile。
# Dockerfile
FROM命令描述了一个基础环境,所以我们不需要从头开始。可以从DockerHub或通过谷歌搜索找到基础镜像的宝库。
RUN命令是一个改变环境的指令。
注意:虽然我们的例子是逐一安装Python库,但这并不推荐。最好的做法是利用requirements.txt,它定义了Python的依赖性。
# Dockerfile with requirements.txt
COPY 命令从你的本地磁盘复制一个文件,像requirements.txt,到镜像中。RUN 命令在这里一次性安装所有在 requirements.txt 中定义的 Python 依赖项。
注意:使用RUN时,所有熟悉的Linux命令都在你的掌握之中。
Docker镜像
现在我们有了我们的Docker文件,我们可以把它编译成一个二进制工件,称为镜像。
这一步的原因是为了让它更快和可重复。如果我们不编译它,每个需要宇航服的人都需要找到一台缝纫机,并不厌其烦地运行每次太空行走的所有指令。这太慢了,但也是不确定的。你的缝纫机可能与我的缝纫机不同。对速度和质量的权衡是,图像可能相当大,往往是千兆字节,但无论如何,2022年的千兆字节是花生米。
要编译,使用build命令。
docker build . -t myimage:1.0
这将建立一个存储在你本地机器上的图像。参数-t将镜像的名称定义为 "myimage",并给它一个标签 "1.0"。要列出所有的镜像,请运行。
docker image list
Docker容器
最后,我们已经准备好进行太空漫步了。容器是太空服的现实生活中的实例。它们在衣柜里并没有真正的帮助,所以宇航员应该在穿着它们的时候执行一两个任务。
这些指示可以烘托在图像中,或者在启动容器之前及时提供。让我们来做后者。
docker run myimagename:1.0 echo "Hello world"
这样就可以启动容器,运行一条回声命令,然后关闭它。
现在我们有一个可重复的方法,可以在任何支持Docker的环境中执行我们的代码。这在数据科学中非常重要,因为每个项目都有许多依赖关系,而可重复性是整个过程的核心所在。
容器在执行完指令后会自动关闭,但容器可以运行很长时间。试着在后台启动一个很长的命令(使用你shell的&操作符)。
docker run myimagename:1.0 sleep 100000000000 &
你可以看到我们当前运行的容器与。
docker container list
要停止这个容器,从表中取出容器的ID并调用。
docker stop <CONTAINER ID>
这就停止了这个容器,但它的状态会被保留下来。如果你调用
docker ps -a
你可以看到这个容器已经停止了,但是仍然存在。要彻底销毁它。
docker rm <CONTAINER ID>
结合了停止和移除的单一命令。
docker rm -f <CONTAINER_ID>
要删除所有停止的剩余的容器。
docker container prune
提示:你也可以用交互式shell来启动一个容器。
$ docker run -it myimagename:1.0 /bin/bash
当你可以自由地交互运行所有的Linux命令时,它对于调试一个镜像的内部工作是非常好的。通过运行exit命令返回到你的主机外壳。
术语和命名
注册处= 用于托管和分发镜像的服务。默认的注册表是Docker Hub。
存储库= 具有相同名称但不同标签的相关镜像的集合。通常,同一应用程序或服务的不同版本。
标签= 储存库中附加在镜像上的标识符(例如,14.04或稳定版)。
ImageID= 为每个图像生成的唯一标识符哈希值
官方文档声明。
镜像名称是由斜线分隔的名称组件组成的,可以选择以注册表主机名为前缀。
这意味着你可以将注册表主机名和一堆斜线分隔的 "名称组件 "编码为你的图像名称。说实话,这很复杂,但这就是生活。
基本的格式是。
<name>:<tag>
但在实践中,它是。
<registry>/<name-component-1>/<name-component-2>:<tag>
每个平台可能有所不同。对于谷歌云平台(GCP)来说,约定俗成的格式是。
<registry>/<project-id>/<repository-name>/<img>@<img-digest>:<tag>
这取决于你是否能找出适合你情况的正确命名方案。

图片由作者提供
注意:如果你拉出一张没有任何标签的图片,就会使用最新的标签。千万不要在生产中使用这个最新的标签。一定要使用带有唯一版本或哈希值的标签,因为不可避免地会有人更新 "最新 "的图片,破坏你的构建。今天是最新的,明天就不再是最新的了。宇航员并不关心最新的铃声和口哨。他们只想要一件适合他们的宇航服,让他们活下去。对于最新的东西,你可能不会得到你所期望的。
Docker图像和秘密
就像把秘密推入git仓库是一种糟糕的做法一样,你也不应该把秘密推入你的Docker镜像中。
镜像被放入存储库,并被不经意地传递。正确的假设是,任何进入镜像的东西都可能在某些时候被公开。它不是存放你的用户名、密码、API令牌、密钥代码、TLS证书或任何其他敏感数据的地方。
秘密和docker镜像有两种情况。
- 你在构建时需要一个秘密
- 你在运行时需要一个秘密
这两种情况都不应该通过在镜像中永久地烘烤东西来解决。让我们来看看如何以不同的方式来解决这个问题。

图片由作者提供
构建时的秘密
如果你需要一些私人的东西--比如一个私人的GitHub仓库--在构建时被拉入镜像,你需要确保你所使用的SSH密钥不会泄漏到镜像中。
不要使用COPY指令将密钥或密码转移到镜像中!即使你事后删除了它们。即使你事后删除了它们,它们仍然会留下痕迹
快速搜索会给你很多不同的选择来解决这个问题,比如使用多阶段构建,但最好的和最现代的方法是使用BuildKit。BuildKit随Docker一起提供,但需要通过设置环境变量DOCKER_BUILDKIT来启用构建。
比如说。
DOCKER_BUILDKIT=1 docker build .
BuildKit提供了一种机制,使秘密文件在构建过程中安全可用。
让我们首先创建带有内容的secret.txt。
TOP SECRET ASTRONAUT PASSWORD
然后创建一个新的Dockerfile。
FROM alpine
--mount=type=secret,id=mypass是通知Docker,对于这个特定的命令,我们需要访问一个叫做mypass的秘密(其内容我们将在下一步告诉Docker构建)。Docker将通过临时挂载一个文件/run/secrets/mypass来实现这一点。
cat /run/secrets/mypass是实际的指令,其中cat是一个Linux命令,用于将文件的内容输出到终端。我们调用它来验证我们的秘密确实是可用的。
让我们构建镜像,加入--secret来告知docker build在哪里可以找到这个秘密。
DOCKER_BUILDKIT=1 docker build . -t myimage \
一切都成功了,但我们并没有像我们预期的那样在终端中看到secret.txt的内容被打印出来。原因是BuildKit默认不会记录每一次成功。
让我们使用额外的参数来构建图像。我们添加BUILDKIT_PROGRESS=plain以获得更多的粗略日志,并添加--no-cache以确保缓存不会破坏它。
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker build . \
在所有打印出来的日志中,你应该找到这一部分。
5# [2/2] RUN --mount=type=secret,id=mypass cat /run/secrets/mypass 5# sha256:7fd248d616c172325af799b6570d2522d3923638ca41181fab4ea143 5# 0.248 TOP SECRET ASTRONAUT PASSWORD
它证明了构建步骤可以访问secret.txt。
有了这种方法,你现在可以安全地将秘密加载到构建过程中,而不必担心将密钥或密码泄露给生成的镜像。
运行时的秘密
如果你需要一个秘密--比如说数据库凭证--当你的容器在生产中运行时,你应该使用环境变量来将秘密传入容器。
千万不要在构建时将任何秘密直接放入镜像中。
docker run --env MYLOGIN=johndoe --env MYPASSWORD=sdf4otwe3789
这些将在Python中被访问。
os.environ.get('MYLOGIN')
提示:你也可以从像Hashicorp Vault这样的秘密商店中获取秘密。
GPU支持
使用GPU的Docker可能很棘手。从头开始构建一个镜像已经超出了本文的范围,但是对于一个现代的GPU(NVIDIA)容器来说,有五个先决条件。
图像。
- CUDA/cuDNN库
- 你的框架的GPU版本,如Tensorflow(必要时)。
主机。
最好的方法是找到一个基本的图像,其中已经包含了大多数先决条件。像Tensorflow这样的框架通常提供像tensorflow/tensorflow:latest-gpu这样的图像,这是一个很好的起点。
在排除故障时,你可以首先尝试测试你的主机。
然后在容器内运行相同的命令。
docker run --gpus all tensorflow/tensorflow:latest-gpu nvidia-smi
你应该得到类似于这两个命令的东西。
如果你从这两个命令中得到一个错误,你就会知道问题是出在容器内部还是外部。
测试你的框架也是一个好主意。比如说Tensorflow。
docker run --gpus all -it --rm \
输出可能是冗长的,并有一些警告,但它应该以类似的内容结束。
Created device /job:localhost/replica:0/task:0/device:GPU:0 with 3006 MB memory: -> device: 0, name: NVIDIA GeForce GTX 970, pci bus id: 0000:01:00.0, compute capability: 5.2 tf.Tensor(-237.35098, shape=(), dtype=float32)
Docker容器vs.Python虚拟环境
Python虚拟环境在你的本地开发环境中的不同Python项目之间创造了一个安全泡。Docker容器解决了一个类似的问题,但在不同的层面上。
Python虚拟环境在所有与Python相关的事物之间建立了隔离层,而Docker容器则为整个软件栈实现了这一点。Python虚拟环境和Docker容器的使用情况是不同的。根据经验,虚拟环境足以在你的本地机器上开发东西,而Docker容器是为在云中运行生产作业而建立的。
换句话说,对于本地开发来说,虚拟环境就像在沙滩上涂抹防晒霜,而Docker容器就像穿上了宇航服--通常不舒服,而且大多不实用。
想要更多实用的工程技巧吗?
数据科学家越来越多地成为研发团队的一部分,并在生产系统上工作,这意味着数据科学和工程领域正在发生碰撞。我想让没有工程背景的数据科学家变得更容易,于是写了一本关于这个主题的免费电子书。