简介
在本实验中,我们将基于实验1的知识展开,在实验1中我们使用Docker命令来运行容器。我们将从一个Dockerfile创建一个自定义的Docker镜像。一旦我们构建了镜像,我们会将其推送到一个中央注册表,在那里它可以被拉取以部署到其他环境中。此外,我们将简要描述镜像层,以及Docker如何结合“写时复制”和联合文件系统来高效地存储镜像和运行容器。
在本实验中,我们将使用一些Docker命令。有关可用命令的完整文档,请查看官方文档。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在本实验中,我们将基于实验1的知识展开,在实验1中我们使用Docker命令来运行容器。我们将从一个Dockerfile创建一个自定义的Docker镜像。一旦我们构建了镜像,我们会将其推送到一个中央注册表,在那里它可以被拉取以部署到其他环境中。此外,我们将简要描述镜像层,以及Docker如何结合“写时复制”和联合文件系统来高效地存储镜像和运行容器。
在本实验中,我们将使用一些Docker命令。有关可用命令的完整文档,请查看官方文档。
Python
应用程序(不使用 Docker)运行以下命令来创建一个名为 app.py
的文件,其中包含一个简单的 Python 程序。(复制粘贴整个代码块)
cd ~/project
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
这是一个简单的 Python 应用程序,它使用 Flask 在端口 5000 上暴露一个 HTTP 网络服务器(5000 是 Flask 的默认端口)。如果你对 Python 或 Flask 不太熟悉也不用担心,这些概念可以应用于用任何语言编写的应用程序。
可选步骤:如果你已经安装了 Python 和 pip,可以在本地运行此应用程序。如果没有,请继续下一步。
$ python3 --version
$ pip3 --version
$ pip3 install flask
$ python3 app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
使用 http://0.0.0.0:5000/
在新的浏览器标签页中打开该应用程序。
现在,如果你本地没有安装 Python 怎么办?别担心!因为你不需要在本地安装。使用容器的优点之一是你可以在容器内部构建 Python 环境,而无需在主机上安装 Python。
通过运行以下命令创建一个 Dockerfile
。(复制粘贴整个代码块)
echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile
一个 Dockerfile 列出了构建 Docker 镜像所需的指令。让我们逐行分析上述文件。
FROM python:3.8-alpine
这是你的 Dockerfile 的起始点。每个 Dockerfile 都必须以 FROM
行开头,这是构建你的镜像层所基于的起始镜像。
在这种情况下,我们选择 python:3.8-alpine
基础层(请参阅 python3.8/alpine3.12 的 Dockerfile),因为它已经包含了运行我们的应用程序所需的 Python 和 pip 版本。
alpine
版本意味着它使用 Alpine Linux 发行版,该发行版比许多其他 Linux 变体小得多,大小约为 8MB,而最小的磁盘安装可能约为 130MB。较小的镜像意味着它将更快地下载(部署),并且在安全方面也有优势,因为它的攻击面较小。Alpine Linux 是一个基于 musl 和 BusyBox 的 Linux 发行版。
在这里,我们使用 “3.8-alpine” 标签来标记 Python 镜像。查看 Docker Hub 上官方 Python 镜像的可用标签。在继承父镜像时使用特定标签是最佳实践,这样可以控制对父依赖项的更改。如果未指定标签,则 “latest” 标签将生效,它充当指向镜像最新版本的动态指针。
出于安全原因,了解你在其上构建 Docker 镜像的层非常重要。因此,强烈建议仅使用在 docker hub 中找到的 “官方” 镜像,或在 docker-store 中找到的非社区镜像。这些镜像经过 审核 以满足某些安全要求,并且也有非常好的文档供用户参考。你可以在 docker hub 上找到有关此 Python 基础镜像 以及所有其他可用镜像的更多信息。
对于更复杂的应用程序,你可能会发现需要使用更高层次的 FROM
镜像。例如,我们的 Python 应用程序的父 Dockerfile 以 FROM alpine
开头,然后为镜像指定一系列 CMD
和 RUN
命令。如果你需要更精细的控制,可以从 FROM alpine
(或其他发行版)开始并自己运行这些步骤。不过,一开始,我建议使用与你需求紧密匹配的官方镜像。
RUN pip install flask
RUN
命令执行设置应用程序镜像所需的命令,例如安装软件包、编辑文件或更改文件权限。在这种情况下,我们正在安装 flask。RUN
命令在构建时执行,并添加到你的镜像层中。
CMD ["python","app.py"]
CMD
是启动容器时执行的命令。在这里,我们使用 CMD
来运行我们的 Python 应用程序。
每个 Dockerfile 只能有一个 CMD
。如果你指定多个 CMD
,则最后一个 CMD
将生效。父镜像 python:3.8-alpine 也指定了一个 CMD
(CMD python3
)。你可以在 这里 找到官方 python:alpine 镜像的 Dockerfile。
你可以直接使用官方 Python 镜像来运行 Python 脚本,而无需在主机上安装 Python。但今天,我们正在创建一个自定义镜像以包含我们的源代码,以便我们可以使用我们的应用程序构建一个镜像并将其部署到其他环境中。
COPY app.py /app.py
这将本地目录(你将在其中运行 docker image build
)中的 app.py 复制到镜像的新层中。此指令是 Dockerfile 中的最后一行。频繁更改的层,例如将源代码复制到镜像中,应放在文件底部以充分利用 Docker 层缓存。这使我们能够避免重建原本可以缓存的层。例如,如果 FROM
指令发生更改,它将使此镜像的所有后续层的缓存无效。我们将在本实验后面演示这一点。
将此指令放在 CMD ["python","app.py"]
行之后似乎违反直觉。请记住,CMD
行仅在容器启动时执行,所以我们在这里不会得到 文件未找到
错误。
这样你就有了一个非常简单的 Dockerfile。你可以在 这里 找到可以放入 Dockerfile 的完整命令列表。现在我们已经定义了 Dockerfile,让我们用它来构建我们的自定义 Docker 镜像。
构建 Docker 镜像。
传入 -t
来将你的镜像命名为 python-hello-world
。
docker image build -t python-hello-world.
验证你的镜像是否出现在你的镜像列表中。
docker image ls
注意 你的基础镜像 python:3.8-alpine
也在你的列表中。
你可以运行历史命令来查看镜像及其层的历史记录,
docker history python-hello-world
docker history python:3.8-alpine
既然你已经构建了镜像,就可以运行它来查看是否能正常工作。
运行 Docker 镜像
docker run -p 5001:5000 -d python-hello-world
-p
标志将容器内运行的端口映射到你的主机。在这种情况下,我们将容器内运行在端口 5000 的 Python 应用程序映射到主机上的端口 5001。请注意,如果端口 5001 已被主机上的另一个应用程序使用,你可能需要将 5001 替换为另一个值,例如 5002。
在终端窗口中导航到 PORTS 标签,并点击链接在新的浏览器标签页中打开应用程序。
在终端中运行 curl localhost:5001
,它将返回 hello world!
。
检查容器的日志输出。
如果你想查看应用程序的日志,可以使用 docker container logs
命令。默认情况下,docker container logs
会打印出应用程序发送到标准输出的内容。使用 docker container ls
来查找正在运行的容器的 ID。
labex:project/ $ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52df977e5541 python-hello-world "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp, :::5001->5000/tcp heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -
Dockerfile 是你为应用程序创建可重复构建的方式。常见的工作流程是让你的 CI/CD 自动化在其构建过程中运行 docker image build
。一旦镜像构建完成,它们将被发送到中央注册表,在那里需要运行该应用程序实例的所有环境(如测试环境)都可以访问。在下一步中,我们将把我们的自定义镜像推送到公共 Docker 注册表:Docker Hub,其他开发者和运维人员可以在那里使用它。
如果你还没有账号,请访问 Docker Hub 并创建一个。或者,你也可以使用 https://quay.io 等。
在本实验中,我们将使用 Docker Hub 作为我们的中央注册表。Docker Hub 是一项免费服务,用于存储公开可用的镜像,或者你也可以付费存储私有镜像。访问 Docker Hub 网站并创建一个免费账号。
大多数大量使用 Docker 的组织会在内部设置自己的注册表。为了简化操作,我们将使用 Docker Hub,但以下概念适用于任何注册表。
登录
你可以在终端中输入 docker login
登录到镜像注册表账户,如果使用 podman,则输入 podman login
。
labex:project/ $ export DOCKERHUB_USERNAME=<your_docker_username>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Password:
WARNING! Your password will be stored unencrypted in /home/labex/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
用你的用户名标记你的镜像
Docker Hub 的命名规范是用 [dockerhub 用户名]/[镜像名] 来标记你的镜像。为此,我们要将之前创建的镜像 python-hello-world
标记为符合该格式。
docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world
将你的镜像推送到注册表
一旦我们有了一个标记正确的镜像,就可以使用 docker push
命令将我们的镜像推送到 Docker Hub 注册表。
docker push $DOCKERHUB_USERNAME/python-hello-world
在浏览器中查看 Docker Hub 上的镜像
访问 Docker Hub,进入你的个人资料,在 https://hub.docker.com/repository/docker/<dockerhub-username>/python-hello-world
查看你新上传的镜像。
现在你的镜像已在 Docker Hub 上,其他开发者和运维人员可以使用 docker pull
命令将你的镜像部署到其他环境。
注意:Docker 镜像包含运行镜像内应用程序所需的所有依赖项。这很有用,因为当我们依赖在每个部署环境中安装的依赖项时,我们不再需要处理环境差异(版本差异)。我们也不必再经历额外的步骤来配置这些环境。只需一步:安装 Docker,然后你就可以开始了。
“你好,世界!” 这个应用程序有点过时了,让我们更新一下应用程序,使其显示 “你好,美丽的世界!”。
app.py
在 app.py
中,将字符串 “Hello World” 替换为 “Hello Beautiful World!”。你可以使用以下命令更新文件。(复制粘贴整个代码块)
echo 'from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello beautiful world!"
if __name__ == "__main__":
app.run(host="0.0.0.0")' > app.py
现在你的应用程序已更新,你需要重复上述步骤来重新构建你的应用程序并将其推送到 Docker Hub 注册表。
首先重新构建,这次在构建命令中使用你的 Docker Hub 用户名:
docker image build -t $DOCKERHUB_USERNAME/python-hello-world.
注意步骤 1 - 3 显示 “使用缓存”。Docker 镜像的这些层已经构建过,docker image build
将使用缓存中的这些层,而不是重新构建它们。
docker push $DOCKERHUB_USERNAME/python-hello-world
推送层时也有一个缓存机制。Docker Hub 已经拥有早期推送中除一层之外的所有层,所以它只推送已更改的那一层。
当你更改一层时,基于该层构建的每一层都必须重新构建。Dockerfile 中的每一行都会构建一个新层,该层是基于它之前的行创建的层构建的。这就是为什么我们 Dockerfile 中行的顺序很重要。我们优化了 Dockerfile,以便最有可能更改的层(COPY app.py /app.py
)是 Dockerfile 的最后一行。一般来说,对于一个应用程序,你的代码变化频率最高。这种优化对于 CI/CD 流程尤为重要,在该流程中你希望自动化尽可能快地运行。
Docker 的主要设计特性之一是其对联合文件系统的使用。
考虑一下我们之前创建的 Dockerfile
:
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py
这些行中的每一行都是一个层。每个层仅包含与它之前的层相比的增量、差异或更改。为了将这些层组合成一个正在运行的容器,Docker 使用联合文件系统将这些层透明地叠加到一个单一视图中。
镜像的每个层都是只读的,除了为正在运行的容器创建的最顶层。读写容器层实现了 “写时复制”,这意味着只有在对存储在较低镜像层中的文件进行编辑时,这些文件才会被提升到读写容器层。然后,这些更改会存储在正在运行的容器层中。“写时复制” 功能非常快,并且在几乎所有情况下,对性能都没有明显影响。你可以使用 docker diff
命令检查哪些文件已被提升到容器级别。有关如何使用 docker diff
的更多信息,请参阅 此处。
由于镜像层是只读的,它们可以被镜像和正在运行的容器共享。例如,使用具有相似基础层的自己的 Dockerfile 创建一个新的 Python 应用程序,将共享它与第一个 Python 应用程序共有的所有层。
FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py
当你从同一个镜像启动多个容器时,你也可以体验到层的共享。由于容器使用相同的只读层,你可以想象启动容器非常快,并且在主机上占用的资源非常少。
你可能会注意到这个 Dockerfile 和你在本实验前面创建的 Dockerfile 中有重复的行。虽然这是一个非常简单的例子,但你可以将两个 Dockerfile 的公共行提取到一个 “基础” Dockerfile 中,然后使用 FROM
命令让每个子 Dockerfile 指向它。
镜像分层为构建和推送启用了 Docker 缓存机制。例如,你最后一次 docker push
的输出表明,你的镜像的某些层已经存在于 Docker Hub 上。
$ docker push $DOCKERHUB_USERNAME/python-hello-world
为了更仔细地查看层,你可以使用我们创建的 Python 镜像的 docker image history
命令。
$ docker image history python-hello-world
每一行代表镜像的一个层。你会注意到顶部的行与你创建的 Dockerfile 匹配,下面的行是从父 Python 镜像中提取的。不用担心 “<缺失>” 标签。这些仍然是正常的层;它们只是没有被 Docker 系统赋予一个 ID。
完成本实验后,你的主机上会有一堆正在运行的容器。让我们清理一下这些容器。
对于每个正在运行的容器,运行 docker container stop [容器ID]
首先使用 docker container ls
获取正在运行的容器列表。
$ docker container ls
然后对列表中的每个容器运行该命令。
$ docker container stop <容器ID>
删除已停止的容器
docker system prune
是一个非常方便的命令,用于清理你的系统。它将删除任何已停止的容器、未使用的卷和网络以及悬空镜像。
$ docker system prune
WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] y
Deleted Containers:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26
Total reclaimed space: 300.3kB
在本实验中,你通过创建自己的自定义 Docker 容器开始创造价值。
关键要点: