高级 Dockerfile 技术

DockerDockerBeginner
立即练习

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

介绍

在本实验中,我们将深入探讨 Dockerfile 的高级技巧,帮助你创建更高效、更灵活的 Docker 镜像。我们将详细介绍 Dockerfile 指令、多阶段构建以及 .dockerignore 文件的使用。此外,我们还将探讨 Docker 镜像中层的核心概念。通过本实验的学习,你将全面掌握这些高级 Dockerfile 技巧,并能够将其应用到自己的项目中。

本实验专为初学者设计,提供了详细的解释,并解决了可能存在的疑惑点。我们将使用 WebIDE(VS Code)进行所有文件编辑任务,使你能够直接在浏览器中轻松创建和修改文件。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL docker(("`Docker`")) -.-> docker/ImageOperationsGroup(["`Image Operations`"]) linux(("`Linux`")) -.-> linux/FileandDirectoryManagementGroup(["`File and Directory Management`"]) linux(("`Linux`")) -.-> linux/BasicFileOperationsGroup(["`Basic File Operations`"]) docker(("`Docker`")) -.-> docker/DockerfileGroup(["`Dockerfile`"]) docker(("`Docker`")) -.-> docker/ContainerOperationsGroup(["`Container Operations`"]) docker/ImageOperationsGroup -.-> docker/images("`List Images`") linux/FileandDirectoryManagementGroup -.-> linux/mkdir("`Directory Creating`") linux/BasicFileOperationsGroup -.-> linux/touch("`File Creating/Updating`") docker/DockerfileGroup -.-> docker/build("`Build Image from Dockerfile`") docker/ContainerOperationsGroup -.-> docker/run("`Run a Container`") docker/ContainerOperationsGroup -.-> docker/ps("`List Running Containers`") docker/ContainerOperationsGroup -.-> docker/logs("`View Container Logs`") docker/ContainerOperationsGroup -.-> docker/inspect("`Inspect Container`") subgraph Lab Skills docker/images -.-> lab-389027{{"`高级 Dockerfile 技术`"}} linux/mkdir -.-> lab-389027{{"`高级 Dockerfile 技术`"}} linux/touch -.-> lab-389027{{"`高级 Dockerfile 技术`"}} docker/build -.-> lab-389027{{"`高级 Dockerfile 技术`"}} docker/run -.-> lab-389027{{"`高级 Dockerfile 技术`"}} docker/ps -.-> lab-389027{{"`高级 Dockerfile 技术`"}} docker/logs -.-> lab-389027{{"`高级 Dockerfile 技术`"}} docker/inspect -.-> lab-389027{{"`高级 Dockerfile 技术`"}} end

理解 Dockerfile 指令与层

让我们从创建一个使用多种指令的 Dockerfile 开始。我们将为使用 Flask 的 Python Web 应用程序构建一个镜像,并在此过程中探索每条指令如何为 Docker 镜像的层做出贡献。

  1. 首先,为我们的项目创建一个新目录。在 WebIDE 终端中运行以下命令:
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

该命令在 project 文件夹内创建一个名为 advanced-dockerfile 的新目录,并切换到该目录。

  1. 现在,让我们创建应用程序文件。在 WebIDE 文件资源管理器(通常位于屏幕左侧)中,右键单击 advanced-dockerfile 文件夹并选择“新建文件”。将此文件命名为 app.py

  2. 打开 app.py 并添加以下 Python 代码:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

这是一个简单的 Flask 应用程序,它会返回一条问候消息,包括它运行的环境。

  1. 接下来,我们需要创建一个 requirements.txt 文件来指定 Python 依赖项。在同一目录下创建一个名为 requirements.txt 的新文件,并添加以下内容:
Flask==2.0.1
Werkzeug==2.0.1

在这里,我们为 Flask 和 Werkzeug 指定了确切的版本,以确保兼容性。

  1. 现在,让我们创建 Dockerfile。在同一目录下创建一个名为 Dockerfile(注意首字母大写)的新文件,并添加以下内容:
## Use an official Python runtime as the base image
FROM python:3.9-slim

## Set the working directory in the container
WORKDIR /app

## Set an environment variable
ENV ENVIRONMENT=production

## Copy the requirements file into the container
COPY requirements.txt .

## Install the required packages
RUN pip install --no-cache-dir -r requirements.txt

## Copy the application code into the container
COPY app.py .

## Specify the command to run when the container starts
CMD ["python", "app.py"]

## Expose the port the app runs on
EXPOSE 5000

## Add labels for metadata
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

现在,让我们分解这些指令,并理解它们如何为 Docker 镜像的层做出贡献:

  • FROM python:3.9-slim:这始终是第一条指令。它指定了我们构建的基础镜像。这创建了镜像的第一层,其中包括 Python 运行时。
  • WORKDIR /app:这为后续指令设置工作目录。它不会创建新层,但会影响后续指令的行为。
  • ENV ENVIRONMENT=production:这设置了一个环境变量。环境变量不会创建新层,但它们存储在镜像的元数据中。
  • COPY requirements.txt .:这将 requirements.txt 文件从主机复制到镜像中。这会创建一个仅包含此文件的新层。
  • RUN pip install --no-cache-dir -r requirements.txt:这会在构建过程中在容器中运行命令。它会安装我们的 Python 依赖项。这会创建一个包含所有已安装包的新层。
  • COPY app.py .:这将我们的应用程序代码复制到镜像中,创建另一个层。
  • CMD ["python", "app.py"]:这指定了容器启动时要运行的命令。它不会创建层,但会设置容器的默认命令。
  • EXPOSE 5000:这实际上只是一种文档形式。它告诉 Docker 容器将在运行时监听此端口,但不会实际发布该端口。它不会创建层。
  • LABEL ...:这些为镜像添加元数据。与 ENV 指令一样,它们不会创建新层,但会存储在镜像的元数据中。

Dockerfile 中的每条 RUNCOPYADD 指令都会创建一个新层。层是 Docker 中的一个基本概念,它允许高效地存储和传输镜像。当你修改 Dockerfile 并重新构建镜像时,Docker 会重用未更改的缓存层,从而加快构建过程。

  1. 现在我们已经理解了 Dockerfile 的作用,让我们构建 Docker 镜像。在终端中运行以下命令:
docker build -t advanced-flask-app .

该命令构建了一个带有标签 advanced-flask-app 的新 Docker 镜像。末尾的 . 告诉 Docker 在当前目录中查找 Dockerfile。

你将看到显示构建过程每个步骤的输出。请注意,每个步骤如何对应 Dockerfile 中的一条指令,以及如果你多次运行构建命令,Docker 如何为未更改的步骤显示“Using cache”。

  1. 构建完成后,我们可以基于新镜像运行一个容器:
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

该命令执行以下操作:

  • -d 以分离模式(后台)运行容器
  • -p 5000:5000 将主机上的 5000 端口映射到容器中的 5000 端口
  • --name flask-container 为我们的新容器命名
  • advanced-flask-app 是我们用于创建容器的镜像

你可以通过检查正在运行的容器列表来验证容器是否正在运行:

docker ps
  1. 要测试我们的应用程序是否正常运行,可以使用 curl 命令:
curl http://localhost:5000

你应该会看到消息“Hello from production environment!”

如果你在使用 curl 时遇到问题,也可以打开一个新的浏览器标签并访问 http://localhost:5000。你应该会看到相同的消息。

如果遇到任何问题,你可以使用以下命令检查容器日志:

docker logs flask-container

这将显示 Flask 应用程序的任何错误消息或输出。

多阶段构建

现在我们已经理解了基本的 Dockerfile 指令和层,接下来让我们探索一种更高级的技术:多阶段构建。多阶段构建允许你在 Dockerfile 中使用多个 FROM 语句。这对于通过仅从一个阶段复制必要的工件到另一个阶段来创建更小的最终镜像特别有用。

让我们修改我们的 Dockerfile,使用多阶段构建来生成一个更小的镜像:

  1. 在 WebIDE 中,打开我们之前创建的 Dockerfile
  2. 将整个内容替换为以下内容:
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

WORKDIR /app

## Copy only the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

CMD ["python", "app.py"]

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

让我们分解这个多阶段 Dockerfile 中发生的事情:

  1. 我们从 builder 阶段开始:

    • 我们使用 Python 3.9-slim 镜像作为基础,从一开始就保持镜像的小巧。
    • 我们在此阶段使用 pip install --user 安装 Python 依赖项。这会将包安装在用户的主目录中。
  2. 然后是我们的最终阶段:

    • 我们使用另一个 Python 3.9-slim 镜像重新开始。
    • 我们仅从 builder 阶段复制已安装的包,特别是从 /root/.local 目录中复制,这是 pip install --user 放置它们的位置。
    • 我们复制应用程序代码。
    • 我们将本地 bin 目录添加到 PATH 中,以便 Python 可以找到已安装的包。
    • 我们像之前一样设置容器的其余部分(ENV、CMD、EXPOSE、LABEL)。

这里的关键优势在于,我们的最终镜像不包含任何构建工具或 pip 安装过程中的缓存。它只包含最终的、必要的工件。这应该会生成一个更小的镜像。

  1. 让我们构建这个新的多阶段镜像。在终端中运行:
docker build -t multi-stage-flask-app .
  1. 构建完成后,让我们比较两个镜像的大小。运行:
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

你现在应该看到 multi-stage-flask-app 比我们之前构建的 advanced-flask-app 更小。

  1. 现在,让我们使用新的、更小的镜像运行一个容器:
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

注意,我们使用了一个不同的主机端口(5001)以避免与之前的容器冲突。

  1. 测试应用程序:
curl http://localhost:5001

你应该仍然会看到消息“Hello from production environment!”

  1. 为了进一步了解单阶段和多阶段镜像之间的差异,我们可以使用 docker history 命令。运行以下命令:
docker history advanced-flask-app
docker history multi-stage-flask-app

比较输出结果。你应该注意到多阶段构建的层数更少,某些层的大小也更小。

多阶段构建是创建高效 Docker 镜像的强大技术。它们允许你在构建过程中使用工具和文件,而不会使最终镜像变得臃肿。这对于编译语言或具有复杂构建过程的应用程序特别有用。

在本例中,我们通过仅复制必要的已安装包和应用程序代码,创建了一个更小的 Python 应用程序镜像,同时丢弃了任何构建工件或缓存。

使用 .dockerignore 文件

在构建 Docker 镜像时,Docker 会将目录中的所有文件发送到 Docker 守护进程。如果你有一些不需要用于构建镜像的大文件,这可能会减慢构建过程。.dockerignore 文件允许你指定在构建 Docker 镜像时应排除的文件和目录。

让我们创建一个 .dockerignore 文件,看看它是如何工作的:

  1. 在 WebIDE 中,在 advanced-dockerfile 目录中创建一个新文件,并将其命名为 .dockerignore
  2. 将以下内容添加到 .dockerignore 文件中:
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

让我们分解这些模式的含义:

  • **/.git:忽略 .git 目录及其所有内容,无论它出现在目录结构中的哪个位置。
  • **/.gitignore:忽略 .gitignore 文件。
  • **/__pycache__:忽略 Python 的缓存目录。
  • **/*.pyc, **/*.pyo, **/*.pyd:忽略编译后的 Python 文件。
  • **/.Python:忽略 .Python 文件(通常由虚拟环境创建)。
  • **/env, **/venv, **/ENV:忽略虚拟环境目录。
  • **/env.bak, **/venv.bak:忽略虚拟环境目录的备份副本。

每行开头的 ** 表示“在任何目录中”。

  1. 为了演示 .dockerignore 文件的效果,让我们创建一些我们希望忽略的文件。在终端中运行:
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

这些命令创建了一个包含文件的 venv 目录和一个 .gitignore 文件。这些是 Python 项目中常见的元素,我们通常不希望它们出现在 Docker 镜像中。

  1. 现在,让我们再次构建我们的镜像:
docker build -t ignored-flask-app .
  1. 为了验证被忽略的文件未包含在构建上下文中,我们可以使用 docker history 命令:
docker history ignored-flask-app

你应该不会看到任何复制 venv 目录或 .gitignore 文件的步骤。

.dockerignore 文件是一个强大的工具,可以保持你的 Docker 镜像干净,并使构建过程更高效。它对于较大的项目特别有用,因为你可能有许多不需要出现在最终镜像中的文件。

高级 Dockerfile 指令

在这最后一步中,我们将探讨一些额外的 Dockerfile 指令和最佳实践,这些可以帮助使你的 Docker 镜像更安全、更易于维护和使用。我们还将专注于故障排除和验证过程的每一步。

  1. 在 WebIDE 中,再次打开 Dockerfile

  2. 将内容替换为以下内容:

## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

## Create a non-root user
RUN useradd -m appuser

WORKDIR /app

## Dynamically determine Python version and site-packages path
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## Copy site-packages and binaries using the variable
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## Set the user to run the application
USER appuser

## Use ENTRYPOINT with CMD
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/ || exit 1

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

让我们分解一下这个 Dockerfile 中引入的新概念:

  • RUN useradd -m appuser:这在容器中创建了一个名为 appuser 的新用户。以非 root 用户身份运行应用程序是一种安全最佳实践,因为它限制了应用程序被攻破时的潜在损害。-m 标志为用户创建了一个主目录。
  • USER appuser:此指令将后续的 RUNCMDENTRYPOINT 指令的用户切换为 appuser。从这一点开始,命令将以 appuser 而不是默认的 root 用户身份执行。
  • RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && ...:这组命令动态确定容器内的 Python 版本,并为 appuser 创建正确的 site-packages 目录。它还为用户的主目录设置了正确的权限。
  • COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}":此指令将安装的 Python 包从 builder 阶段复制到最终镜像中动态确定的 site-packages 路径,确保包放置在 appuser 可以使用的正确位置。
  • COPY --from=builder /root/.local/bin /home/appuser/.local/bin:这将由 pip 安装的可执行脚本(如 Flask 的命令行界面,如果有的话)从 builder 阶段复制到 appuser 的本地 bin 目录。
  • ENTRYPOINT ["python"]CMD ["app.py"]:当一起使用时,ENTRYPOINT 定义了容器的主要可执行文件(在本例中为 python),而 CMD 提供了该可执行文件的默认参数(app.py)。这种模式允许灵活性:用户可以运行容器并默认执行 app.py,或者他们可以覆盖 CMD 以运行其他 Python 脚本或命令。
  • HEALTHCHECK:此指令为容器配置健康检查。Docker 将定期执行指定的命令(curl -f http://localhost:5000/)以确定容器是否健康。--interval=30s--timeout=3s 标志分别设置了检查间隔和超时时间。如果 curl 命令失败(返回非零退出代码),则容器被视为不健康。
  • ARG BUILD_VERSION:这定义了一个名为 BUILD_VERSION 的构建参数。构建参数允许你在构建时将值传递到 Docker 镜像中。
  • LABEL version="${BUILD_VERSION:-1.0}":这在 Docker 镜像上设置了一个名为 version 的标签。它使用 BUILD_VERSION 构建参数。如果在构建过程中提供了 BUILD_VERSION,则使用其值;否则,默认为 1.0(使用 :- 默认值语法)。
  1. 现在,让我们构建这个新镜像,并指定一个构建版本:
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .

--build-arg BUILD_VERSION=2.0 标志允许我们在镜像构建过程中为 BUILD_VERSION 构建参数传递值 2.0。此值将用于设置 Docker 镜像中的 version 标签。

  1. 构建完成后,让我们验证镜像是否成功创建:
docker images | grep advanced-flask-app-v2

你应该在 docker images 命令的输出中看到新镜像 advanced-flask-app-v2,以及它的标签、镜像 ID、创建日期和大小。

  1. 现在,让我们使用新镜像运行一个容器:
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2

此命令以分离模式(-d)运行容器,将主机上的端口 5002 映射到容器中的端口 5000(-p 5002:5000),将容器命名为 advanced-container-v2--name advanced-container-v2),并使用 advanced-flask-app-v2 镜像创建容器。

  1. 让我们验证容器是否正在运行:
docker ps | grep advanced-container-v2

如果容器成功运行,你应该在 docker ps 命令的输出中看到它。如果没有看到容器列出,它可能已经退出。让我们检查是否有任何停止的容器:

docker ps -a | grep advanced-container-v2

如果你在 docker ps -a 的输出中看到容器列出但没有运行(状态不是 "Up"),我们可以检查其日志以查找错误:

docker logs advanced-container-v2

此命令将显示 advanced-container-v2 容器的日志,这可以帮助诊断 Flask 应用程序中的任何启动问题或运行时错误。

  1. 假设容器正在运行,给它一些时间启动后,我们可以检查其健康状态:
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

在短暂的延迟后(以允许健康检查至少运行一次),你应该看到 "healthy" 作为输出。如果你最初看到 "unhealthy",请等待 30 秒(健康检查间隔)并再次运行该命令。如果它仍然 "unhealthy",请使用 docker logs advanced-container-v2 检查容器日志,以查找 Flask 应用程序中的潜在问题。如果没有明显问题,则可以忽略。

  1. 我们还可以验证我们的构建版本标签是否正确应用:
docker inspect -f '{{.Config.Labels.version}}' advanced-flask-app-v2

此命令从 advanced-flask-app-v2 镜像中检索 version 标签的值并显示它。你应该看到 "2.0" 作为输出,这确认了 BUILD_VERSION 构建参数已正确用于设置标签。

  1. 最后,让我们通过向应用程序发送请求来测试它:
curl http://localhost:5002

你应该在输出中看到消息 "Hello from production environment!"。这表明你的 Flask 应用程序在 Docker 容器内正确运行,并且可以在主机的端口 5002 上访问。

这些高级技术使你能够创建更安全、可配置且适合生产的 Docker 镜像。非 root 用户提高了安全性,HEALTHCHECK 有助于容器编排和监控,构建参数允许更灵活和版本化的镜像构建。

总结

在本实验中,我们探索了高级 Dockerfile 技术,这些技术将帮助你创建更高效、更安全且更易于维护的 Docker 镜像。我们涵盖了以下内容:

  1. 详细的 Dockerfile 指令及其对镜像层的影响:我们学习了每条指令如何为 Docker 镜像的结构做出贡献,以及理解层如何帮助我们优化镜像。
  2. 多阶段构建:我们使用这种技术将构建环境与运行时环境分离,从而创建更小的最终镜像。
  3. 使用 .dockerignore 文件:我们学习了如何从构建上下文中排除不必要的文件,这可以加快构建速度并减小镜像大小。
  4. 高级 Dockerfile 指令:我们探索了额外的指令,如 USERENTRYPOINTHEALTHCHECKARG,这些指令使我们能够创建更安全和灵活的镜像。

这些技术使你能够:

  • 创建更优化且更小的 Docker 镜像
  • 通过以非 root 用户身份运行应用程序来提高安全性
  • 实现健康检查以更好地进行容器编排
  • 使用构建时变量以实现更灵活的镜像构建

在整个实验中,我们使用 WebIDE(VS Code)来编辑文件,这使得可以直接在浏览器中轻松创建和修改 Dockerfile 和应用程序代码。这种方法为使用 Docker 提供了无缝的开发体验。

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