在微服务领域,拥有安全、高效和紧凑的 Docker 映像对于成功部署至关重要。本博客将探讨有助于构建此类映像的关键因素,包括不以 root 用户身份运行映像的重要性、在构建映像时更新和升级包、在编写 Dockerfile 指令时考虑 Docker 的层架构,以及利用多阶段构建来减小映像大小。本博客的部分内容受到我最近来自Matthijs Brouns的精彩演讲的启发,您可以在此处查看。
我将使用 CLI 工具潜水来分析图像及其图层。Docker镜像的目的是充当机器学习应用程序的FASTAPI服务器的主机,而Poetry是其依赖项管理器。有关使用诗歌而不是pip/pipenv/pip-tools/conda的动机的更多信息,请阅读此博客和此博客。请注意,对 Docker 和 Docker 文件的基本了解是本文的先决条件。
FROM python:3.11-slim as build
我正在使用 Python 3.11 的精简版本来最小化容器的大小并使其尽可能轻量级。它的大小为 121 MB。
完整的Python映像(python:3.11)包括运行应用程序不需要的额外开发工具和文档,使其更大,大小为875 MB,比苗条版本大约7倍。
另一种选择是Alpine(python:3.11-alpine),它更小,大小为56.5 MB。但是,它缺少软件包安装程序 pip 和对安装轮子包的支持,这两者都是安装 Pandas 和 Numpy 等应用程序所必需的。若要安装这些应用程序,需要使用 G++ 等编译器包从源文件编译它们,默认情况下也不会在 Alpine 映像上安装这些包。这导致图像尺寸比苗条版本更大(和麻烦得多),所以让我们继续那个。
ENV PIP_DEFAULT_TIMEOUT=100 \
# Allow statements and log messages to immediately appear
PYTHONUNBUFFERED=1 \
# disable a pip version check to reduce run-time & log-spam
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# cache is useless in docker image, so disable to reduce image size
PIP_NO_CACHE_DIR=1
ARG POETRY_VERSION=1.3.2
Dockerfile 中的 ENV 变量被设置为在 Docker 容器中安装软件包期间优化 pip 和诗歌的行为。此外,我明确定义了要安装的诗歌版本。
让我们继续讨论安全性,因为有几个方面很重要。不建议以 root 用户身份运行 Docker 容器,因为 root 用户可以完全控制主机系统,包括修改或删除文件、启动和停止服务以及访问敏感信息的能力。若要遵循最小特权原则,最好仅使用所需的最低特权运行容器。因此,为了增强安全性,我们为 Docker 容器创建了一个名为 的非 root 用户。appuser
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& adduser --system --uid 1001 --gid 1001 --no-create-home appuser \
通过分配特定的用户和组 ID(例如 1001),可以更轻松地管理不同系统之间的用户权限和访问控制,尤其是在 Kubernetes 等平台上运行容器时。
那么 的权限与根用户的权限有何不同呢?分配给的权限可能包括对某些文件和目录的读取、写入或执行访问的限制,具体取决于为这些资源设置的默认权限。例如,默认情况下,可能没有对容器中特定子目录的写入访问权限。如果运行 作为的应用程序需要对该目录的写入访问权限,则需要使用以下命令更改该目录的所有权,以授予所需的权限:appuser
appuser
appuser
appuser
chown
appuser
RUN chown -R appuser:appuser /your-subdirectory
否则将引发以下错误:
[Errno 13] Permission denied: 'your-subdirectory/filename'
另一个安全最佳实践是更新和升级 Docker 容器中的包。原因是,在 Docker 环境中,用于创建容器的基础映像通常是特定版本的操作系统的快照。随着时间的推移,可能会发现安全漏洞或其他问题,并发布修补程序以使用 和 解决这些问题。该命令更新包索引(有点像可用软件包及其元数据的数据库),并从包存储库中检索最新的包信息。apt-get update
apt-get upgrade
apt-get update
RUN set -ex \
&& apt-get update \
&& apt-get upgrade -y
现在我们有了有关可用软件包的信息,我们可以使用该命令将容器中当前安装的软件包升级到其最新的可用版本。该标志用于在升级过程中自动对任何提示回答“是”。apt-get upgrade
-y
⬆ 更新过程导致一些自动安装的包、包缓存文件和包索引文件。在运行应用程序时,不再需要这些文件,因为它们达到了帮助升级包的目的。我们可以使用以下命令安全地删除这些文件,这应该会减小 Docker 映像的大小。
# Clean up
RUN set -ex apt-get autoremove -y \\
&& apt-get clean -y \\
&& rm -rf /var/lib/apt/lists/*
最后的安全最佳做法是确保 docker 映像中不包含任何机密。为了帮助防止这种情况,您可以将常用机密文件和文件夹添加到 .dockerignore 文件中:
**/.env
**/.aws
**/.ssh
您还可以使用 Trivy,例如,当您使用“root”作为图像用户时,它会警告您。以下是VSCode扩展的屏幕截图:
好了,现在我们已经设置了一些环境变量并提高了 docker 容器的安全性,让我们继续复制实际的应用程序文件并安装依赖项。我们希望将工作目录设置为“/app”。此目录在基本映像中尚不存在,但如果不存在,则会为我们创建它。所有后续指令都将在此位置执行,这使我们的 docker 镜像更有条理和可移植性。WORKDIR
WORKDIR /app
COPY pyproject.toml poetry.lock ./
该命令用于将文件从主机系统复制到容器文件系统。在本例中,我们正在复制 和 ,这是 Poetry 包管理器的配置文件。COPY
pyproject.toml
poetry.lock
现在我们已经完成了一些步骤,让我们构建 docker 镜像并深入研究它以检查️ ♀️不同的层:
docker build -t app:latest .
dive app:latest
我们可以在每一层中看到与前一层相比进行了哪些更改。首先,我们使用命令创建了一个应用程序目录,然后复制两个文件。WORKDIR
当前映像的大小为 139 MB。它看起来相当不错,但可以进行一些改进以减小其大小。首先要记住的是,系统中后续层删除的文件实际上仍然存在于图像中;它们只是在最后一层无法访问。
因此,分别安装安全更新和删除现在不必要的文件和包的步骤不会节省任何空间。起作用的是将这两个步骤合二为一。
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& adduser --system --uid 1001 --gid 1001 --no-create-home appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
这将图像的大小从 18 mb 减小到 总大小 121 mb ,这与我们之前的图层完全相同。apt-get update && update-get upgrade -y
好的,让我们继续!安装依赖项后,我们会将我的应用程序需要的一些子目录复制到容器中。接下来是使用 Poetry 安装 python 包:
COPY ./artifacts artifacts
COPY ./api api
RUN pip install "poetry==$POETRY_VERSION" \
&& poetry install --no-root --no-ansi --no-interaction
但是这些命令的顺序有意义吗?请记住,每个层都是与其下方层独立的增量。每次更改图层时,它都会更改其后的每个图层。如果上一层与之前完全相同,我们可以只使用缓存的值,而不是重建该步骤。
Step 12/18 : COPY ./api api
---> Using cache
---> ea3ba41d1a13
因此,您希望将图层从最不可能更改到最有可能更改进行排序,以缩短构建时间。在复制程序的其余部分之前先安装依赖项是有意义的。与应用程序代码相比,更新和更改依赖项的可能性通常要小得多。 因此,这是一个更好的指令顺序:
RUN pip install "poetry==$POETRY_VERSION" \
&& poetry install --no-root --no-ansi --no-interaction
COPY ./artifacts artifacts
COPY ./api api
下一步是公开一个端口供我们的应用程序侦听。 在 Dockerfile 中更多地用于文档目的,指示应用程序在容器中侦听哪些端口。EXPOSE
为了实际公开我们的 FASTAPI 服务器,我们运行以下命令:
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
最后,我们使用命令设置应用程序将运行的用户。这设置为我们之前创建的用户 .USER
appuser
让我们再次构建 docker 镜像并检查大小:
映像现在的总大小为 731 MB。531 mb 来自 Poetry 安装的依赖项,但 Poetry 本身也占用了一些空间。
尽管 Poetry 在开发阶段对于创建虚拟环境和管理依赖项很有用,但在运行 Docker 映像时不需要这些功能,因为映像提供了自己的隔离环境,并且我们的依赖项已经安装。因此,减小 Docker 映像的大小也可以通过确保 Poetry 包不包含在最终映像中来实现。
多阶段构建可用于从最终的 Docker 映像中排除 Poetry,因为它可以从单个 Dockerfile 创建多个映像。与直接使用 Poetry 安装依赖项不同,Poetry 还可以在构建阶段将必要的依赖项导出到 needs.txt 文件中。此文件可以复制到最后阶段,并由 pip 用于安装依赖项。
FROM python:3.11-slim as build
. . .
RUN pip install "poetry==$POETRY_VERSION" \
&& poetry install --no-root --no-ansi --no-interaction \
&& poetry export -f requirements.txt -o requirements.txt
### Final stage
FROM python:3.11-slim as final
WORKDIR /app
COPY --from=build /app/requirements.txt .
RUN pip install -r requirements.txt
通过在最后阶段排除诗歌,Docker图像的大小减小,如从732 MB减少到538 MB 所示。
FROM python:3.11-slim as build
ENV PIP_DEFAULT_TIMEOUT=100 \
# Allow statements and log messages to immediately appear
PYTHONUNBUFFERED=1 \
# disable a pip version check to reduce run-time & log-spam
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# cache is useless in docker image, so disable to reduce image size
PIP_NO_CACHE_DIR=1 \
POETRY_VERSION=1.3.2
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install "poetry==$POETRY_VERSION" \
&& poetry install --no-root --no-ansi --no-interaction \
&& poetry export -f requirements.txt -o requirements.txt
### Final stage
FROM python:3.11-slim as final
WORKDIR /app
COPY --from=build /app/requirements.txt .
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& adduser --system --uid 1001 --gid 1001 --no-create-home appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
# Install dependencies
&& pip install -r requirements.txt \
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
COPY ./artifacts artifacts
COPY ./api api
EXPOSE 8000
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Set the user to run the application
USER appuser
总之,通过在创建 Dockerfile 时遵循这些提示和最佳实践,您可以确保最终映像针对安全性、大小和性能进行了优化。
确保选择正确的基础映像,了解 Docker 层的不变性,按正确的顺序放置说明,并遵循安全最佳实践。
如何使您的 Python Docker 镜像安全、快速和小巧 |作者:比约恩·范·迪克曼 |华帝人工智能 |中等 (medium.com)