本文着眼于编写 Dockerfile 和使用 Docker 时应遵循的一些最佳实践。尽管列出的大多数实践适用于所有开发人员,无论使用哪种语言,但少数实践仅适用于开发基于 Python 的应用程序的开发人员。
Dockerfiles
使用多阶段构建
利用多阶段构建来创建更精简、更安全的 Docker 映像。
多阶段 Docker 构建允许您将 Dockerfile 分解为多个阶段。例如,您可以有一个用于编译和构建应用程序的阶段,然后可以将其复制到后续阶段。由于只使用最后阶段来创建映像,因此与构建应用程序相关的依赖项和工具将被丢弃,留下一个精益且模块化的生产就绪映像。
网页开发示例:
# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# final stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*
在此示例中,安装某些 Python 包需要GCC编译器,因此我们添加了一个临时的构建时间阶段来处理构建阶段。由于最终的运行时映像不包含 GCC,因此它更轻、更安全。
尺寸比较:
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-single latest 8d6b6a4d7fb6 16 seconds ago 259MB
docker-multi latest 813c2fa9b114 3 minutes ago 156MB
数据科学示例:
# temp stage
FROM python:3.9 as builder
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels jupyter pandas
# final stage
FROM python:3.9-slim
WORKDIR /notebooks
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/*
尺寸比较:
REPOSITORY TAG IMAGE ID CREATED SIZE
ds-multi latest b4195deac742 2 minutes ago 357MB
ds-single latest 7c23c43aeda6 6 minutes ago 969MB
总之,多阶段构建可以减小生产映像的大小,帮助您节省时间和金钱。此外,这将简化您的生产容器。此外,由于较小的尺寸和简单性,可能存在较小的攻击面。
注意 Dockerfile 命令顺序
密切注意 Dockerfile 命令的顺序以利用层缓存。
Docker 将每个步骤(或层)缓存在特定的 Dockerfile 中,以加快后续构建。当一个步骤发生变化时,缓存将不仅对该特定步骤而且所有后续步骤都将失效。
例子:
FROM python:3.9-slim
WORKDIR /app
COPY sample.py .
COPY requirements.txt .
RUN pip install -r /requirements.txt
在这个 Dockerfile 中,我们在安装需求之前复制了应用程序代码。现在,每次我们更改sample.py时,构建都会重新安装包。这是非常低效的,尤其是在使用 Docker 容器作为开发环境时。因此,将经常更改的文件保留在 Dockerfile 的末尾至关重要。
您还可以通过使用.dockerignore文件排除不必要的文件添加到 Docker 构建上下文和最终映像来帮助防止不必要的缓存失效。更多关于这个很快在这里。
因此,在上面的 Dockerfile 中,您应该将COPY sample.py .
命令移到底部:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r /requirements.txt
COPY sample.py .
笔记:
- 始终将可能更改的层放在 Dockerfile 中尽可能低的位置。
- 组合
RUN apt-get update
和RUN apt-get install
命令。(这也有助于减小图像大小。我们稍后会谈到这一点。) - 如果要关闭特定 Docker 构建的缓存,请添加
--no-cache=True
标志。
使用小型 Docker 基础镜像
较小的 Docker 镜像更加模块化和安全。
使用较小的图像构建、推送和拉取图像更快。它们也往往更安全,因为它们只包含运行应用程序所需的必要库和系统依赖项。
你应该使用哪个 Docker 基础镜像?
不幸的是,这取决于。
下面是 Python 的各种 Docker 基础镜像的大小比较:
REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.9.6-alpine3.14 f773016f760e 3 days ago 45.1MB
python 3.9.6-slim 907fc13ca8e7 3 days ago 115MB
python 3.9.6-slim-buster 907fc13ca8e7 3 days ago 115MB
python 3.9.6 cba42c28d9b8 3 days ago 886MB
python 3.9.6-buster cba42c28d9b8 3 days ago 886MB
虽然基于Alpine Linux的 Alpine 版本是最小的,但如果您找不到可以使用它的编译二进制文件,它通常会导致构建时间增加。因此,您最终可能不得不自己构建二进制文件,这可能会增加映像大小(取决于所需的系统级依赖项)和构建时间(由于必须从源代码编译)。
请参阅适用于您的 Python 应用程序的最佳 Docker 基础镜像和使用 Alpine 可以使 Python Docker 构建速度降低 50 倍,以详细了解为什么最好避免使用基于 Alpine 的基础镜像。
最后,一切都是为了平衡。如有疑问,请从*-slim
风格开始,尤其是在开发模式下,因为您正在构建应用程序。当您添加新的 Python 包时,您希望避免必须不断更新 Dockerfile 以安装必要的系统级依赖项。当您为生产强化应用程序和 Dockerfile(s) 时,您可能希望探索使用 Alpine 从多阶段构建中获取最终映像。
此外,不要忘记定期更新基础镜像以提高安全性和性能。当基础镜像的新版本发布时——即,
3.9.6-slim
->3.9.7-slim
——你应该拉取新镜像并更新你正在运行的容器以获取所有最新的安全补丁。
最小化层数
尽可能多地组合 、 和 命令是个好主意,因为它们会RUN
创建COPY
图层。ADD
每一层都增加了图像的大小,因为它们被缓存了。因此,随着层数的增加,尺寸也会增加。
您可以使用以下docker history
命令对此进行测试:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerfile latest 180f98132d02 51 seconds ago 259MB
$ docker history 180f98132d02
IMAGE CREATED CREATED BY SIZE COMMENT
180f98132d02 58 seconds ago COPY . . # buildkit 6.71kB buildkit.dockerfile.v0
<missing> 58 seconds ago RUN /bin/sh -c pip install -r requirements.t… 35.5MB buildkit.dockerfile.v0
<missing> About a minute ago COPY requirements.txt . # buildkit 58B buildkit.dockerfile.v0
<missing> About a minute ago WORKDIR /app
...
注意尺寸。只有RUN
、COPY
和ADD
命令会增加图像的大小。您可以通过尽可能组合命令来减小图像大小。例如:
RUN apt-get update
RUN apt-get install -y netcat
可以组合成一个RUN
命令:
RUN apt-get update && apt-get install -y netcat
因此,创建单层而不是两层,从而减小了最终图像的大小。
虽然减少层数是一个好主意,但更重要的是,这本身不是一个目标,而是减少图像大小和构建时间的副作用。换句话说,更多地关注前三个实践——多阶段构建、Dockerfile 命令的顺序以及使用小型基础映像——而不是尝试优化每个命令。
笔记:
RUN
,COPY
, 和ADD
每个创建层。- 每一层都包含与前一层的差异。
- 层增加了最终图像的大小。
提示:
- 组合相关命令。
step
在创建它们的同一 RUN 中删除不必要的文件。- 最小化运行次数,
apt-get upgrade
因为它将所有软件包升级到最新版本。 - 使用多阶段构建,不必担心过度优化临时阶段的命令。
最后,为了可读性,最好按字母数字对多行参数进行排序:
RUN apt-get update && apt-get install -y \
git \
gcc \
matplotlib \
pillow \
&& rm -rf /var/lib/apt/lists/*
使用非特权容器
默认情况下,Docker 在容器内以 root 身份运行容器进程。但是,这是一种不好的做法,因为在容器内以 root 身份运行的进程在 Docker 主机中以 root 身份运行。因此,如果攻击者获得了对您容器的访问权限,他们就可以访问所有 root 权限,并且可以对 Docker 主机执行多次攻击,例如 -
- 将敏感信息从主机的文件系统复制到容器
- 执行远程命令
为防止这种情况,请确保以非 root 用户运行容器进程:
RUN addgroup --system app && adduser --system --group app
USER app
您可以更进一步,删除 shell 访问并确保也没有主目录:
RUN addgroup --gid 1001 --system app && \
adduser --no-create-home --shell /bin/false --disabled-password --uid 1001 --system --group app
USER app
核实:
$ docker run -i sample id
uid=1001(app) gid=1001(app) groups=1001(app)
在这里,容器内的应用程序在非 root 用户下运行。但是,请记住,Docker 守护进程和容器本身仍以 root 权限运行。请务必查看以非 root 用户身份运行 Docker 守护程序以获取有关以非 root 用户身份运行守护程序和容器的帮助。
首选复制而不是添加
除非COPY
您确定需要ADD
.
COPY
和有什么区别ADD
?
这两个命令都允许您将文件从特定位置复制到 Docker 映像中:
ADD <src> <dest>
COPY <src> <dest>
虽然它们看起来具有相同的目的,ADD
但具有一些附加功能:
COPY
用于将本地文件或目录从 Docker 主机复制到镜像。ADD
可用于相同的事情以及下载外部文件。此外,如果您使用压缩文件(tar、gzip、bzip2 等)作为<src>
参数,ADD
将自动将内容解压缩到给定位置。
# copy local files on the host to the destination
COPY /source/path /destination/path
ADD /source/path /destination/path
# download external file and copy to the destination
ADD http://external.file/url /destination/path
# copy and extract local compresses files
ADD source.file.tar.gz /destination/path
将 Python 包缓存到 Docker 主机
当需求文件发生更改时,需要重新构建映像以安装新软件包。前面的步骤将被缓存,如最小化层数中所述。在重建映像时下载所有包可能会导致大量网络活动并花费大量时间。每次重新构建都需要相同的时间来跨构建下载通用包。
您可以通过将 pip 缓存目录映射到主机上的目录来避免这种情况。因此,对于每次重建,缓存的版本都会持续存在并可以提高构建速度。
将卷添加到 docker run 作为-v $HOME/.cache/pip-docker/:/root/.cache/pip
或作为 Docker Compose 文件中的映射。
以上目录仅供参考。确保映射缓存目录而不是站点包(构建包所在的位置)。
将缓存从 docker 映像移动到主机可以节省最终映像中的空间。
如果您使用Docker BuildKit,请使用 BuildKit 缓存挂载来管理缓存:
# syntax = docker/dockerfile:1.2
...
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
...
每个容器只运行一个进程
为什么建议每个容器只运行一个进程?
假设您的应用程序堆栈由两个 Web 服务器和一个数据库组成。虽然您可以轻松地从单个容器中运行所有三个,但您应该在单独的容器中运行每个,以便更容易重用和扩展每个单独的服务。
- 扩展- 每个服务都在一个单独的容器中,您可以根据需要水平扩展您的一个 Web 服务器以处理更多流量。
- 可重用性——也许您有另一个需要容器化数据库的服务。您可以简单地重用同一个数据库容器,而无需同时带来两个不必要的服务。
- 日志记录- 耦合容器使日志记录更加复杂。我们将在本文后面更详细地讨论这个问题。
- 可移植性和可预测性- 当需要处理的表面积较小时,制作安全补丁或调试问题要容易得多。
优先使用数组而不是字符串语法
您可以在 Dockerfile 中以数组 (exec) 或字符串 (shell) 格式编写CMD
和命令:ENTRYPOINT
# array (exec)
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "main:app"]
# string (shell)
CMD "gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app"
两者都是正确的,并且实现了几乎相同的目标;但是,您应该尽可能使用 exec 格式。从Docker 文档:
- 确保您在 Dockerfile 中
CMD
使用exec 形式。ENTRYPOINT
- 例如使用
["program", "arg1", "arg2"]
not"program arg1 arg2"
。使用字符串形式会导致 Docker 使用 bash 运行您的进程,这不能正确处理信号。Compose 始终使用 JSON 格式,因此如果您覆盖 Compose 文件中的命令或入口点,请不要担心。
因此,由于大多数 shell 不处理子进程的信号,因此如果您使用 shell 格式,CTRL-C
(生成 a SIGTERM
)可能不会停止子进程。
例子:
FROM ubuntu:18.04
# BAD: shell format
ENTRYPOINT top -d
# GOOD: exec format
ENTRYPOINT ["top", "-d"]
试试这两个。请注意,使用 shell 格式风格,CTRL-C
不会终止进程。相反,您会看到^C^C^C^C^C^C^C^C^C^C^C
.
另一个需要注意的是,shell 格式带有 shell 的 PID,而不是进程本身。
# array format
root@18d8fd3fd4d2:/app# ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 python manage.py runserver 0.0.0.0:8000
7 ? Sl 0:02 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
25 pts/0 Ss 0:00 bash
356 pts/0 R+ 0:00 ps ax
# string format
root@ede24a5ef536:/app# ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/sh -c python manage.py runserver 0.0.0.0:8000
8 ? S 0:00 python manage.py runserver 0.0.0.0:8000
9 ? Sl 0:01 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
13 pts/0 Ss 0:00 bash
342 pts/0 R+ 0:00 ps ax
了解 ENTRYPOINT 和 CMD 之间的区别
我应该使用 ENTRYPOINT 还是 CMD 来运行容器进程?
在容器中运行命令有两种方式:
CMD ["gunicorn", "config.wsgi", "-b", "0.0.0.0:8000"]
# and
ENTRYPOINT ["gunicorn", "config.wsgi", "-b", "0.0.0.0:8000"]
两者本质上都做同样的事情:config.wsgi
使用 Gunicorn 服务器启动应用程序并将其绑定到0.0.0.0:8000
.
CMD
很容易被覆盖。如果你运行docker run <image_name> uvicorn config.asgi
,上面的 CMD 会被新的参数取代——例如,uvicorn config.asgi
. 而要覆盖ENTRYPOINT
命令,必须指定--entrypoint
选项:
docker run --entrypoint uvicorn config.asgi <image_name>
在这里,很明显我们正在覆盖入口点。因此,建议使用ENTRYPOINT
overCMD
以防止意外覆盖命令。
它们也可以一起使用。
例如:
ENTRYPOINT ["gunicorn", "config.wsgi", "-w"]
CMD ["4"]
像这样一起使用时,启动容器的命令是:
gunicorn config.wsgi -w 4
如上所述,CMD
很容易被覆盖。因此,CMD
可用于将参数传递给ENTRYPOINT
命令。工人的数量可以很容易地改变,如下所示:
docker run <image_name> 6
这将以六个 Gunicorn 工人而不是四个启动容器。
包括 HEALTHCHECK 说明
使用 aHEALTHCHECK
来确定容器中运行的进程是否不仅已启动并正在运行,而且是否“健康”。
Docker 公开了一个 API 用于检查容器中运行的进程的状态,它提供的信息远不止进程是否“运行”,因为“运行”包括“它已启动并工作”、“仍在启动”、甚至“陷入某种无限循环错误状态”。您可以通过HEALTHCHECK指令与此 API 进行交互。
例如,如果您正在为 Web 应用程序提供服务,则可以使用以下内容来确定/
端点是否已启动并可以处理服务请求:
HEALTHCHECK CMD curl --fail http://localhost:8000 || exit 1
如果运行docker ps
,就可以看到状态了HEALTHCHECK
。
健康的例子:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09c2eb4970d4 healthcheck "python manage.py ru…" 10 seconds ago Up 8 seconds (health: starting) 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp xenodochial_clarke
不健康的例子:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09c2eb4970d4 healthcheck "python manage.py ru…" About a minute ago Up About a minute (unhealthy) 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp xenodochial_clarke
您可以更进一步,设置一个仅用于运行状况检查的自定义端点,然后将其配置HEALTHCHECK
为针对返回的数据进行测试。例如,如果端点返回 JSON 响应{"ping": "pong"}
,您可以指示HEALTHCHECK
验证响应正文。
以下是您使用以下方式查看运行状况检查状态的方法docker inspect
:
❯ docker inspect --format "{{json .State.Health }}" ab94f2ac7889
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2021-09-28T15:22:57.5764644Z",
"End": "2021-09-28T15:22:57.7825527Z",
"ExitCode": 0,
"Output": "..."
在这里,输出被修剪,因为它包含整个 HTML 输出。
您还可以向 Docker Compose 文件添加运行状况检查:
version: "3.8"
services:
web:
build: .
ports:
- '8000:8000'
healthcheck:
test: curl --fail http://localhost:8000 || exit 1
interval: 10s
timeout: 10s
start_period: 10s
retries: 3
选项:
test
:要测试的命令。interval
:要测试的间隔——即,测试每个x
单位时间。timeout
:等待响应的最长时间。start_period
: 何时开始健康检查。它可以在容器准备好之前执行其他任务时使用,例如运行迁移。retries
:在将测试指定为 之前的最大重试次数failed
。
如果您使用的是 Docker Swarm 以外的编排工具(即 Kubernetes 或 AWS ECS),则该工具很可能有自己的内部系统来处理健康检查。
HEALTHCHECK
在添加指令之前,请参阅特定工具的文档。
镜像
版本 Docker 映像
尽可能避免使用latest
标签。
如果您依赖latest
标签(它不是真正的“标签”,因为它在未明确标记图像时默认应用),您无法根据图像标签判断正在运行的代码版本。它使回滚变得具有挑战性,并且很容易覆盖它(无论是意外还是恶意)。标签,就像你的基础设施和部署一样,应该是不可变的。
无论您如何处理内部映像,都不应将latest
标记用于基本映像,因为您可能会无意中部署新版本,并对生产进行重大更改。
对于内部镜像,使用描述性标签可以更容易地判断正在运行的代码版本、处理回滚并避免命名冲突。
例如,您可以使用以下描述符来组成标签:
- 时间戳
- Docker 镜像 ID
- Git 提交哈希
- 语义版本
有关更多选项,请查看“Properly Versioning Docker Images” Stack Overflow 问题中的答案。
例如:
docker build -t web-prod-a072c4e5d94b5a769225f621f08af3d4bf820a07-0.1.4 .
在这里,我们使用以下内容来形成标签:
- 项目名:
web
- 环境名称:
prod
- Git提交哈希:
a072c4e5d94b5a769225f621f08af3d4bf820a07
- 语义版本:
0.1.4
选择一个标记方案并与之保持一致至关重要。由于提交哈希可以轻松地将图像标签快速绑定回代码,因此强烈建议将它们包含在您的标签方案中。
不要在镜像中存储秘密
机密是敏感信息,例如密码、数据库凭据、SSH 密钥、令牌和 TLS 证书等等。这些不应该在未经加密的情况下被烘焙到您的图像中,因为获得图像访问权限的未经授权的用户只能检查层以提取秘密。
不要以明文形式向 Dockerfile 添加机密,尤其是当您将图像推送到像Docker Hub这样的公共注册表时:
FROM python:3.9-slim
ENV DATABASE_PASSWORD "SuperSecretSauce"
相反,它们应该通过以下方式注入:
- 环境变量(运行时)
- 构建时参数(在构建时)
- 像 Docker Swarm(通过 Docker 机密)或 Kubernetes(通过 Kubernetes 机密)这样的编排工具
此外,您可以通过将常见的秘密文件和文件夹添加到.dockerignore文件来帮助防止泄露秘密:
**/.env
**/.aws
**/.ssh
最后,明确哪些文件被复制到图像中,而不是递归地复制所有文件:
# BAD
COPY . .
# GOOD
copy ./app.py .
显式也有助于限制缓存破坏。
环境变量
您可以通过环境变量传递秘密,但它们将在所有子进程、链接容器和日志中可见,也可以通过docker inspect
. 更新它们也很困难。
$ docker run --detach --env "DATABASE_PASSWORD=SuperSecretSauce" python:3.9-slim
d92cf5cf870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239
$ docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' d92cf5cf870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239
DATABASE_PASSWORD=SuperSecretSauce
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
PYTHON_VERSION=3.9.7
PYTHON_PIP_VERSION=21.2.4
PYTHON_SETUPTOOLS_VERSION=57.5.0
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/c20b0cfd643cd4a19246ccf204e2997af70f6b21/public/get-pip.py
PYTHON_GET_PIP_SHA256=fa6f3fb93cce234cd4e8dd2beb54a51ab9c247653b52855a48dd44e6b21ff28b
这是最直接的秘密管理方法。虽然它不是最安全的,但它可以让诚实的人保持诚实,因为它提供了一层薄薄的保护,有助于防止好奇的流浪眼睛隐藏秘密。
使用共享卷传递秘密是一个更好的解决方案,但它们应该通过Vault或AWS Key Management Service (KMS) 进行加密,因为它们被保存到磁盘中。
构建时参数
您可以使用build-time arguments在构建时传递秘密,但那些有权访问图像的人可以看到它们docker history
。
例子:
FROM python:3.9-slim
ARG DATABASE_PASSWORD
建造:
$ docker build --build-arg "DATABASE_PASSWORD=SuperSecretSauce" .
如果您只需要临时使用机密作为构建的一部分——即,用于克隆私有 repo 或下载私有包的 SSH 密钥——您应该使用多阶段构建,因为构建器历史在临时阶段被忽略:
# temp stage
FROM python:3.9-slim as builder
# secret
ARG SSH_PRIVATE_KEY
# install git
RUN apt-get update && \
apt-get install -y --no-install-recommends git
# use ssh key to clone repo
RUN mkdir -p /root/.ssh/ && \
echo "${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa
RUN touch /root/.ssh/known_hosts &&
ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts
RUN git clone git@github.com:testdrivenio/not-real.git
# final stage
FROM python:3.9-slim
WORKDIR /app
# copy the repository from the temp image
COPY --from=builder /your-repo /app/your-repo
# use the repo for something!
多阶段构建仅保留最终图像的历史记录。请记住,您可以将此功能用于应用程序所需的永久机密,例如数据库凭证。
您还可以使用--secret
Docker 构建中的新选项将机密传递给未存储在映像中的 Docker 映像。
# "docker_is_awesome" > secrets.txt
FROM alpine
# shows secret from default secret location:
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
这将从secrets.txt
文件中挂载秘密。
构建镜像:
docker build --no-cache --progress=plain --secret id=mysecret,src=secrets.txt .
# output
...
#4 [1/2] FROM docker.io/library/alpine
#4 sha256:665ba8b2cdc0cb0200e2a42a6b3c0f8f684089f4cd1b81494fbb9805879120f7
#4 CACHED
#5 [2/2] RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
#5 sha256:75601a522ebe80ada66dedd9dd86772ca932d30d7e1b11bba94c04aa55c237de
#5 0.635 docker_is_awesome#5 DONE 0.7s
#6 exporting to image
最后,检查历史,看看秘密是否泄露:
❯ docker history 49574a19241c
IMAGE CREATED CREATED BY SIZE COMMENT
49574a19241c 5 minutes ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 5 minutes ago RUN /bin/sh -c cat /run/secrets/mysecret # b… 0B buildkit.dockerfile.v0
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:aad4290d27580cc1a… 5.6MB
有关构建时机密的更多信息,请查看不要泄露 Docker 映像的构建机密。
docker secret
如果您使用的是Docker Swarm,则可以使用Docker secrets管理机密。
例如,初始化 Docker Swarm 模式:
$ docker swarm init
创建一个docker secret:
$ echo "supersecretpassword" | docker secret create postgres_password -
qdqmbpizeef0lfhyttxqfbty0
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
qdqmbpizeef0lfhyttxqfbty0 postgres_password 4 seconds ago 4 seconds ago
当容器被授予访问上述机密的权限时,它将挂载在/run/secrets/postgres_password
. 该文件将以明文形式包含密钥的实际值。
使用不同的整理工具?
- AWS EKS -将 AWS Secrets Manager 密钥与 Kubernetes 结合使用
- DigitalOcean Kubernetes -保护 DigitalOcean Kubernetes 集群的推荐步骤
- Google Kubernetes Engine -将 Secret Manager 与其他产品一起使用
- Nomad - Vault 集成和检索动态机密
使用 .dockerignore 文件
我们已经多次提到使用.dockerignore文件。该文件用于指定您不想添加到初始构建上下文中的文件和文件夹,这些文件和文件夹发送到 Docker 守护程序,然后它将构建您的映像。换句话说,您可以使用它来定义您需要的构建上下文。
构建 Docker 映像时,整个 Docker 上下文(即项目的根目录)在评估 or 命令之前被发送到Docker守护进程。这可能非常昂贵,特别是如果您的项目中有许多依赖项、大型数据文件或构建工件。另外,Docker CLI 和守护进程可能不在同一台机器上。因此,如果守护程序在远程机器上执行,您应该更加注意构建上下文的大小。COPY
ADD
您应该在.dockerignore文件中添加什么?
- 临时文件和文件夹
- 构建日志
- 本地秘密
- 本地开发文件,如docker-compose.yml
- 版本控制文件夹,如“.git”、“.hg”和“.svn”
例子:
**/.git
**/.gitignore
**/.vscode
**/coverage
**/.env
**/.aws
**/.ssh
Dockerfile
README.md
docker-compose.yml
**/.DS_Store
**/venv
**/env
总之,结构合理的.dockerignore可以帮助:
- 减小 Docker 镜像的大小
- 加快构建过程
- 防止不必要的缓存失效
- 防止泄露秘密
Lint 并扫描您的 Dockerfile 和图像
Linting 是检查源代码中是否存在可能导致潜在缺陷的程序和风格错误以及不良做法的过程。就像编程语言一样,静态文件也可以被检查。特别是对于您的 Dockerfile,linter 可以帮助确保它们是可维护的、避免不推荐使用的语法并遵守最佳实践。对图像进行检查应该是 CI 管道的标准部分。
Hadolint是最流行的 Dockerfile linter:
$ hadolint Dockerfile
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:7 DL3042 warning: Avoid the use of cache directory with pip. Use `pip install --no-cache-dir <package>`
Dockerfile:9 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile:17 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT arguments
您可以在https://hadolint.github.io/hadolint/在线查看它的实际效果。还有一个VS Code Extension。
您可以将 Dockerfile 与扫描图像和容器的漏洞结合起来。
一些选项:
- Snyk是 Docker 原生漏洞扫描的独家提供商。您可以使用
docker scan
CLI 命令扫描图像。 - Trivy可用于扫描容器镜像、文件系统、git 存储库和其他配置文件。
- Clair是一个开源项目,用于静态分析应用程序容器中的漏洞。
- Anchore是一个开源项目,为容器镜像的检查、分析和认证提供集中式服务。
总之,检查并扫描您的 Dockerfile 和图像以发现任何偏离最佳实践的潜在问题。
签署和验证图像
你怎么知道用于运行生产代码的图像没有被篡改?
篡改可以通过中间人(MITM) 攻击或完全被破坏的注册表来进行。
Docker Content Trust (DCT) 支持从远程注册表对 Docker 映像进行签名和验证。
要验证图像的完整性和真实性,请设置以下环境变量:
DOCKER_CONTENT_TRUST=1
现在,如果您尝试提取尚未签名的图像,您将收到以下错误:
Error: remote trust data does not exist for docker.io/namespace/unsigned-image:
notary.docker.io does not have trust data for docker.io/namespace/unsigned-image
您可以从Signing Images with Docker Content Trust文档中了解有关签署图像的信息。
从 Docker Hub 下载图像时,请确保使用来自可信来源的官方图像或经过验证的图像。较大的团队应该考虑使用他们自己的内部私有容器注册表。
温馨提示
使用 Python 虚拟环境
你应该在容器内使用虚拟环境吗?
在大多数情况下,只要您坚持每个容器只运行一个进程,就不需要虚拟环境。由于容器本身提供隔离,因此可以在系统范围内安装包。也就是说,您可能希望在多阶段构建中使用虚拟环境,而不是构建轮文件。
带wheel的例子:
# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# final stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*
使用 virtualenv 的示例:
# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt
# final stage
FROM python:3.9-slim
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
设置内存和 CPU 限制
限制 Docker 容器的内存使用是一个好主意,尤其是当您在单台机器上运行多个容器时。这可以防止任何容器使用所有可用内存,从而削弱其余容器。
限制内存使用的最简单方法是在 Docker cli中使用--memory
和选项:--cpu
$ docker run --cpus=2 -m 512m nginx
上述命令将容器使用限制为 2 个 CPU 和 512 兆字节的主内存。
您可以在 Docker Compose 文件中执行相同的操作,如下所示:
version: "3.9"
services:
redis:
image: redis:alpine
deploy:
resources:
limits:
cpus: 2
memory: 512M
reservations:
cpus: 1
memory: 256M
注意reservations
字段。它用于设置软限制,当主机内存或 CPU 资源不足时优先。
其他资源:
记录到标准输出或标准错误
在 Docker 容器中运行的应用程序应将日志消息写入标准输出 (stdout) 和标准错误 (stderr) 而不是文件。
然后,您可以配置 Docker 守护程序以将日志消息发送到集中式日志记录解决方案(如CloudWatch Logs或Papertrail)。
有关更多信息,请查看The Twelve-Factor App中的将日志视为事件流和Docker 文档中的配置日志记录驱动程序。
为 Gunicorn Heartbeat 使用共享内存挂载
Gunicorn 使用基于文件的心跳系统来确保所有分叉的工作进程都处于活动状态。
在大多数情况下,心跳文件位于“/tmp”中,通常通过tmpfs在内存中。由于 Docker 默认不利用 tmpfs,因此文件将存储在磁盘支持的文件系统上。这可能会导致问题,例如由于心跳系统使用随机冻结os.fchmod
,如果目录实际上位于磁盘支持的文件系统上,则可能会阻止工作人员。
--worker-tmp-dir
幸运的是,有一个简单的解决方法:通过标志将心跳目录更改为内存映射目录。
gunicorn --worker-tmp-dir /dev/shm config.wsgi -b 0.0.0.0:8000
结论
本文着眼于使您的 Dockerfile 和镜像更清洁、更精简和更安全的几个最佳实践。