高级 Dockerfile 技巧

DockerBeginner
立即练习

介绍

在本实验中,我们将深入探讨 Dockerfile 技巧,学习能帮助你创建更高效、更灵活的 Docker 镜像的高级概念。我们将涵盖详细的 Dockerfile 指令、多阶段构建(multi-stage builds)以及 .dockerignore 文件的使用。我们还将探讨 Docker 镜像中至关重要的「层」(layers)概念。通过本实验,你将对这些高级 Dockerfile 技巧有全面的理解,并能将其应用到自己的项目中。

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

理解 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 .:这从宿主机复制需求文件到镜像中。这会创建一个仅包含此文件的新层。
  • 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 指令和层,那么让我们探索一种更高级的技巧:多阶段构建(multi-stage builds)。多阶段构建允许你在 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 阶段复制已安装的软件包,具体是从 pip install --user 放置它们的 /root/.local 目录复制。
    • 我们复制应用程序代码。
    • 我们将本地 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 守护进程(daemon)。如果你有一些构建镜像不需要的大文件,这会减慢构建过程。.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

## Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

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 标志会为该用户创建一个主目录。
  • RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*:这安装了 curl 软件包,它是我们的 HEALTHCHECK 指令工作所必需的。我们还清理了 apt 缓存以减小镜像体积。
  • 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 应用程序是否存在潜在问题。如果没有明显问题,你可以忽略「unhealthy」状态。

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

此命令从 advanced-container-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 指令:我们探索了额外的指令,如 USER、ENTRYPOINT、HEALTHCHECK 和 ARG,它们允许我们创建更安全、更灵活的镜像。

这些技巧使你能够:

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

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