使用自定义Docker镜像创造价值

DockerDockerBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在本实验中,我们将基于实验1的知识展开,在实验1中我们使用Docker命令来运行容器。我们将从一个Dockerfile创建一个自定义的Docker镜像。一旦我们构建了镜像,我们会将其推送到一个中央注册表,在那里它可以被拉取以部署到其他环境中。此外,我们将简要描述镜像层,以及Docker如何结合“写时复制”和联合文件系统来高效地存储镜像和运行容器。

在本实验中,我们将使用一些Docker命令。有关可用命令的完整文档,请查看官方文档


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL docker(("`Docker`")) -.-> docker/ContainerOperationsGroup(["`Container Operations`"]) docker(("`Docker`")) -.-> docker/ImageOperationsGroup(["`Image Operations`"]) docker(("`Docker`")) -.-> docker/SystemManagementGroup(["`System Management`"]) docker(("`Docker`")) -.-> docker/DockerfileGroup(["`Dockerfile`"]) docker/ContainerOperationsGroup -.-> docker/run("`Run a Container`") docker/ContainerOperationsGroup -.-> docker/ls("`List Containers`") docker/ContainerOperationsGroup -.-> docker/logs("`View Container Logs`") docker/ImageOperationsGroup -.-> docker/push("`Push Image to Repository`") docker/ImageOperationsGroup -.-> docker/images("`List Images`") docker/SystemManagementGroup -.-> docker/prune("`Remove Unused Docker Objects`") docker/DockerfileGroup -.-> docker/build("`Build Image from Dockerfile`") subgraph Lab Skills docker/run -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/ls -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/logs -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/push -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/images -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/prune -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} docker/build -.-> lab-148983{{"`使用自定义Docker镜像创造价值`"}} end

创建一个 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/ 在新的浏览器标签页中打开该应用程序。

Flask app browser output

创建并构建 Docker 镜像

现在,如果你本地没有安装 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 应用程序的父 DockerfileFROM alpine 开头,然后为镜像指定一系列 CMDRUN 命令。如果你需要更精细的控制,可以从 FROM alpine(或其他发行版)开始并自己运行这些步骤。不过,一开始,我建议使用与你需求紧密匹配的官方镜像。

RUN pip install flask
RUN 命令执行设置应用程序镜像所需的命令,例如安装软件包、编辑文件或更改文件权限。在这种情况下,我们正在安装 flask。RUN 命令在构建时执行,并添加到你的镜像层中。

CMD ["python","app.py"]
CMD 是启动容器时执行的命令。在这里,我们使用 CMD 来运行我们的 Python 应用程序。

每个 Dockerfile 只能有一个 CMD。如果你指定多个 CMD,则最后一个 CMD 将生效。父镜像 python:3.8-alpine 也指定了一个 CMDCMD 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 镜像

docker run -p 5001:5000 -d python-hello-world

-p 标志将容器内运行的端口映射到你的主机。在这种情况下,我们将容器内运行在端口 5000 的 Python 应用程序映射到主机上的端口 5001。请注意,如果端口 5001 已被主机上的另一个应用程序使用,你可能需要将 5001 替换为另一个值,例如 5002。

在终端窗口中导航到 PORTS 标签,并点击链接在新的浏览器标签页中打开应用程序。

Terminal ports tab link

在终端中运行 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 的更多信息,请参阅 此处

understanding image layers

由于镜像层是只读的,它们可以被镜像和正在运行的容器共享。例如,使用具有相似基础层的自己的 Dockerfile 创建一个新的 Python 应用程序,将共享它与第一个 Python 应用程序共有的所有层。

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py
understanding image layers

当你从同一个镜像启动多个容器时,你也可以体验到层的共享。由于容器使用相同的只读层,你可以想象启动容器非常快,并且在主机上占用的资源非常少。

你可能会注意到这个 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 容器开始创造价值。

关键要点:

  • Dockerfile 是你为应用程序创建可重复构建的方式,以及你将应用程序与 Docker 集成到持续集成/持续交付(CI/CD)管道的方式。
  • Docker 镜像可以通过中央注册表供你所有的环境使用。Docker Hub 是注册表的一个示例,但你可以在自己控制的服务器上部署自己的注册表。
  • Docker 镜像包含运行镜像内应用程序所需的所有依赖项。这很有用,因为当我们依赖在每个部署环境中安装的依赖项时,我们不再需要处理环境差异(版本差异)。
  • Docker 使用联合文件系统和 “写时复制” 来重用镜像层。这降低了存储镜像的占用空间,并显著提高了启动容器的性能。
  • 镜像层由 Docker 构建和推送系统缓存。无需重新构建或重新推送目标系统上已有的镜像层。
  • Dockerfile 中的每一行都会创建一个新层,并且由于层缓存,更改更频繁的行(例如向镜像中添加源代码)应列在文件底部附近。

您可能感兴趣的其他 Docker 教程