什么是 Docker?
想象一下自己是空间站上的宇航员,并计划到户外欣赏美景。你将面临不利的条件,温度、氧气和辐射等不是为你而创建的。人类需要特定的环境才能茁壮成长。为了在任何其他场景中正常运行,例如:在深海或高空,我们需要一个系统来重现该环境。 无论是宇航服还是潜艇,我们都需要隔离和确保我们所依赖的氧气、压力和温度这些东西。换句话说,我们需要一个容器。
任何软件都面临与宇航员相同的问题。 一旦我们离开家并走向世界,环境就会变得充满敌意,并且必须有一种保护机制来再现我们的自然环境。 Docker 容器是程序的太空服。
Docker 将软件与同一系统上的所有其他事物隔离开来。 在“宇航服”内运行的程序通常不知道自己穿着宇航服,也不会受到外面发生的任何事情的影响。
容器堆栈
- 应用程序:高层应用程序(您的数据科学项目)
- 依赖项:低层通用软件(想想 Tensorflow 或 Python)
- Docker 容器:隔离层
- 操作系统:与硬件交互的低层接口和驱动程序
- 硬件:CPU、内存、硬盘、网络等
基本思想是将应用程序及其依赖项打包成一个可重用的工件,该工件可以在不同的环境中可靠地实例化。
如何创建容器?
创建 Docker 容器的流程:
- Dockerfile:编译镜像的说明
- 镜像:编译的制品
- 容器:镜像的执行实例
Dockerfile
首先,我们需要指令。
我们可以定义宇航服的温度、辐射和氧气水平,但我们需要指令。 Docker 是基于指令的。 为此,我们将创建一个文本文件并将其命名为 Dockerfile。
# Dockerfile FROM python:3.9 RUN pip install tensorflow==2.7.0 RUN pip install pandas==1.3.3 复制代码
FROM
命令描述了一个基本环境,所以我们不需要从头开始。 可以从 DockerHub 或通过谷歌搜索找到基础镜像的宝库。
RUN
命令是改变环境的指令。
注意:虽然我们的示例会一一安装 Python 库,但不建议这样做。 最佳实践是使用 requirements.txt
,它定义了 Python 依赖项。
# Dockerfile with requirements.txt FROM python:3.9 COPY requirements.txt /tmp RUN pip install -r /tmp/requirements.txt 复制代码
COPY
命令将文件从本地磁盘复制到镜像中,例如:requirements.txt
。 此处的 RUN
命令一次性安装 requirements.txt
中定义的所有 Python 依赖项。
注意:使用 RUN
时,您可以随意使用所有熟悉的 Linux 命令。
Docker 镜像
现在我们有了 Dockerfile,我们可以将它编译成一个称为镜像的二进制制品。
此步骤的原因是使其更快且可重复。如果我们不编译它,每个需要太空服的人都需要找到一台缝纫机,并为每次太空行走煞费苦心地运行所有指令。这太慢了,但也不确定。你的缝纫机可能和我的不一样,镜像可能非常大,通常是千兆字节,但 2022 年的千兆字节是微不足道的。
要编译,请使用 build
命令:
docker build . -t myimage:1.0 复制代码
这将构建存储在本地计算机上的镜像。 -t
参数将镜像名称定义为“myimage”并给它一个标签“1.0”。
要列出所有镜像,请运行:
docker image list REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 85eb1ea6d4be 6 days ago 2.9GB myimagename 1.0 ff732d925c6e 6 days ago 2.9GB myimagename 1.1 ff732d925c6e 6 days ago 2.9GB myimagename latest ff732d925c6e 6 days ago 2.9GB python 3.9 f88f0508dc46 13 days ago 912MB 复制代码
Docker 容器
最后,我们准备好进行太空行走了。 容器是太空服的真实实例。 但它们在衣橱里并没有真正的有用,所以,宇航员应该在穿着它们时,执行一两个任务。
指令可以烘焙到镜像中,也可以在启动容器之前及时提供,启动容器之前提供具体指令如下所示。
docker run myimagename:1.0 echo "Hello world" 复制代码
这将启动容器,运行单个 echo 命令,然后将其关闭。
现在我们有了一个可重现的方法来在任何支持 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 root@9c4060d0136e:/# echo "hello" hello root@9c4060d0136e:/# exit exit $ <back in the host shell> 复制代码
当您可以交互地自由运行所有 Linux 命令时,它非常适合调试镜像的内部工作。 通过运行 exit
命令返回到您的主机外壳。
术语和命名
Registry : 用于托管和分发镜像的服务。默认注册表是 Docker Hub
。
Repository : 具有相同名称但不同标签的相关镜像的集合。通常,同一应用程序或服务的不同版本。
Tag : 附加到Repository中镜像的标识符(例如: 14.04 或 stable )。
官方文件声明:
镜像名称由斜杠分隔的名称组件组成,可以选择以注册表主机名作为前缀。
这意味着您可以将注册表主机名和一组斜杠分隔的“名称组件”编码到你的镜像的名称中。老实说,这很令人费解,但这就是生活。
基本格式为:
<name>:<tag> 复制代码
但在实践中它是:
<registry>/<name-component-1>/<name-component-2>:<tag> 复制代码
它可能因平台而异。 对于 Google Cloud Platform (GCP),约定是:
<registry>/<project-id>/<repository-name>/<image>@<image-digest>:<tag> 复制代码
您可以为您的案例找出正确的命名方案。
注意:如果您拉取没有任何标签的镜像,将使用 latest
标签。 切勿在生产中使用此“latest”标签。 始终使用具有唯一版本或哈希的标签,因为有人不可避免地会更新“latest”镜像并破坏您的构建。 今天最新的不再是明天最新的了! 宇航员并不关心最新的花里胡哨的东西。 他们只想要一件适合他们并让他们保持活力的宇航服。 使用 latest
,您可能无法获得预期的结果。
Docker 镜像和 secrets
就像将 secrets 推送到 git 存储库中是一种糟糕的做法一样,您也不应该将它们烘焙到您的 Docker 镜像中!
镜像被放入存储库并漫不经心地传递。 正确的假定是镜像中的任何内容都可能在某些时候是公开的。 它不是存放您的用户名、密码、API 令牌、密钥代码、TLS 证书或任何其他敏感数据的地方。
secrets 和 docker 镜像有两种场景:
- 你在构建时需要一个secrets
- 您在运行时需要一个secrets
这两种情况都不应该通过将东西永久地烘焙到镜像中来解决。 让我们看看如何以不同的方式进行操作。
构建时的 secrets
如果你需要一些私有的东西,比如,一个私有的 GitHub 存储库。在构建时被拉入镜像,你需要确保你使用的 SSH 密钥不会泄漏到镜像中。
不要使用 COPY 指令将密钥或密码移动到镜像中!就算你后来把它们去掉,它们仍然会留下痕迹!
快速谷歌搜索将为您提供许多不同的选项来解决此问题,例如:使用多阶段构建,但最好和最现代的方法是使用 BuildKit。 BuildKit 随 Docker 一起提供,但需要通过设置环境变量 DOCKER_BUILDKIT
来启用构建。
例如:
DOCKER_BUILDKIT=1 docker build . 复制代码
BuildKit 提供了一种机制,使secret文件可安全地用于构建过程。
让我们首先使用以下内容创建 secret.txt
:
TOP SECRET ASTRONAUT PASSWORD 复制代码
然后,创建一个新的 Dockerfile
:
FROM alpine RUN --mount=type=secret,id=mypass cat /run/secrets/mypass 复制代码
--mount=type=secret,id=mypass
通知 Docker 对于这个特定的命令,我们需要访问一个名为 mypass 的 secret(我们将在下一步中告诉 Docker 构建的内容)。 Docker 将通过临时挂载文件/run/secrets/mypass
来实现这一点。
cat /run/secrets/mypass
是实际指令,其中, cat 是将文件内容输出到终端的 Linux 命令。 我们调用它来验证我们的 secret 确实可用。
让我们构建镜像,添加 --secret
来通知 docker build
在哪里可以找到这个 secret:
DOCKER_BUILDKIT=1 docker build . -t myimage --secret id=mypass,src=secret.txt 复制代码
一切正常,但我们没有看到我们的终端打印出我们预期的 secret.txt
的内容。 原因是 BuildKit 默认不会记录所有成功。
让我们使用其他参数构建镜像。 我们添加了BUILDKIT_PROGRESS=plain
以获得更详细的日志记录和使用--no-cache
以确保缓存不会破坏它:
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker build . --no-cache --secret id=mypass,src=secret.txt 复制代码
在所有打印出来的日志中,你应该可以找到这部分:
5# [2/2] RUN --mount=type=secret,id=mypass cat /run/secrets/mypass 5# sha256:7fd248d616c172325af799b6570d2522d3923638ca41181fab438c29d0aea143 5# 0.248 TOP SECRET ASTRONAUT PASSWORD 复制代码
这证明构建步骤可以访问 secret.txt
。
使用这种方法,您现在可以安全地将 secret 挂载到构建过程中,而不必担心将密钥或密码泄漏到生成的镜像中。
运行时的 secrets
如果你需要一个 secret ,比如:数据库凭证。当你的容器在生产中运行时,你应该使用环境变量将 secret 传递到容器中。
永远不要在构建时将任何 secret 直接烘焙到镜像中!
docker run --env MYLOGIN=johndoe --env MYPASSWORD=sdf4otwe3789 复制代码
这些将可以在 Python 中访问,例如:
os.environ.get('MYLOGIN') os.environ.get('MYPASSWORD') 复制代码
提示:您还可以从 Hashicorp Vault
等 secret 商店获取 secrets!
GPU 支持
带有 GPU 的 Docker 可能会很棘手。 从头开始构建镜像超出了本文的范围,但现代 GPU (NVIDIA) 容器有五个先决条件。
镜像:
- CUDA/cuDNN 库
- 你的框架的 GPU 版本,如:Tensorflow
主机:
- GPU 驱动
- NVidia Docker 工具集
- 使用
--gpus all
执行 Dockerrun
命令
最好的方法是找到一个包含大多数先决条件的基础镜像。像 Tensorflow 这样的框架通常会提供像 tensorflow/tensorflow:latest-gpu
这样的镜像,这是一个很好的开始。
故障排除时,可以先尝试测试一下你的宿主机:
nvidia-smi 复制代码
然后在容器内运行相同的命令:
docker run --gpus all tensorflow/tensorflow:latest-gpu nvidia-smi 复制代码
对于这两个命令,您都应该得到类似的结果:
如果您从其中任何一个中得到错误,您就会知道问题出在容器内部还是外部。
测试你的框架也是一个好主意。 例如,Tensorflow:
docker run --gpus all -it --rm tensorflow/tensorflow:latest-gpu python -c "import tensorflow as tf;print(tf.reduce_sum(tf.random.normal([1000, 1000])))" 复制代码
输出可能很冗长并且有一些警告,但它应该以如下内容结束:
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 容器与 Python 虚拟环境对比
我们之前在关于 Python 依赖管理 的文章中讨论了 Python 虚拟环境以及它们如何在您本地的不同 Python 项目之间创建安全气泡的开发环境。 Docker 容器解决了类似的问题,但在不同的层上。
Python 虚拟环境在所有与 Python 相关的事物之间创建了隔离层,而 Docker 容器为整个软件堆栈实现了这一点。 Python 虚拟环境和 Docker 容器的用例不同。 根据经验,虚拟环境足以在本地机器上开发东西,而 Docker 容器是为在云中运行生产作业而构建的。
换句话说,对于本地开发来说,虚拟环境就像在海滩上涂防晒霜,而 Docker 容器就像穿着宇航服(通常不舒服,而且大多不切实际)。