构建更快,部署更智能:立即优化您的 Docker 设置
您准备好提升您的云和 DevOps 技能了吗?
🐥《云原生devops》专门为您打造,我们精心打造的 30 篇文章库,这些文章涵盖了 Azure、AWS 和 DevOps 方法论的众多重要主题。无论您是希望精进专业知识的资深专业人士,还是渴望学习相关知识的新手,这套资源库都能满足您的需求。
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
————————————————
Docker 是一款强大的容器化工具,但构建速度缓慢可能会影响开发周期。以下是一些可操作的技巧和步骤,可帮助您优化 Docker 构建速度。
1. 使用多阶段构建
描述:多阶段构建允许您在 Dockerfile 中使用多个 FROM 语句。这样,您可以避免在最终镜像中包含不必要的文件,从而减小镜像大小并加快构建速度。
示例:
FROM eclipse-temurin:21.0.2_13-jdk-jammy
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY src ./src
CMD ["./mvnw", "spring-boot:run"]
没有使用多阶段的Dockerfile,构建后的镜像大小为540M。
ninjamac@ninjamacdeMacBook-Air spring-boot-docker % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-helloworld latest 538dd687fed5 16 minutes ago 540MB
FROM eclipse-temurin:21.0.2_13-jdk-jammy AS builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean installFROM eclipse-temurin:21.0.2_13-jre-jammy AS final
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
使用多阶段Dockerfile构建的镜像大小只有290M。
ninjamac@ninjamacdeMacBook-Air spring-boot-docker % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-helloworld-multistage latest 06fa85d3415a About a minute ago 291MB
spring-helloworld latest 538dd687fed5 16 minutes ago 540MB
2. 利用 Docker 缓存
说明:Docker 逐层构建镜像,并缓存每一层。您可以将不常更改的指令(例如安装依赖项)放在最顶层,从而充分利用这一点。
提示:始终将依赖项的 COPY 命令放在应用程序代码之前。
示例:
Dockerfile
FROM node:14
WORKDIR /app# Install dependencies first
COPY package.json package-lock.json ./
RUN npm install# Then copy the application code
COPY . .CMD ["npm", "start"]
3. 最小化层数
说明:Dockerfile 中的每个命令都会创建一个新的层。通过将命令分组来最小化层数。
提示:使用 && 组合 RUN 命令。
示例:
Dockerfile
FROM ubuntu:latest
RUN apt-get update && \apt-get install -y git curl && \rm -rf /var/lib/apt/lists/*
4. 有效使用 .dockerignore
描述:与 .gitignore 类似,.dockerignore 文件会告知 Docker 在构建上下文中应忽略哪些文件/文件夹,从而减少发送到 Docker 守护进程的不必要数据。
示例:创建如下 .dockerignore 文件:
node_modules
*.log
*.tmp
.git
5. 减小镜像大小
描述:镜像越小,构建和推送所需的时间就越短。请尽可能使用精简的基础镜像(例如 Alpine)。
示例:
Dockerfile
FROM alpine:latest
RUN apk add --no-cache python3
6. 优化软件包安装
描述:尽量减少安装的软件包数量,并使用支持缓存或更快安装的软件包管理器。
提示:安装软件包时请使用特定版本,以避免在构建过程中重复下载。
示例:
Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends git=1:2.25.1-1ubuntu3
7. 使用 Docker BuildKit 缓存依赖项
描述:启用 BuildKit 以获得更好的缓存和构建性能。
命令:
DOCKER_BUILDKIT=1 docker build -t myapp .
8. 使用构建参数
描述:使用构建参数自定义构建,这有助于避免重复下载。
示例:
Dockerfile
ARG NODE_VERSION=14
FROM node:${NODE_VERSION}
9. 创建可复用阶段
如果您有多个镜像,并且它们有很多共同点,请考虑创建一个包含共享组件的可复用阶段,并以此为基础创建您的专属阶段。Docker 只需构建一次通用阶段。这意味着您的衍生镜像可以更高效地利用 Docker 主机上的内存,并更快地加载。
维护一个通用的基础阶段(“不要重复自己”)也比使用多个不同的阶段执行类似的操作更容易。
10.解耦应用程序
每个容器应该只关注一个问题。将应用程序解耦到多个容器中,可以更轻松地进行水平扩展和容器复用。例如,一个 Web 应用程序堆栈可能由三个独立的容器组成,每个容器都有自己独特的镜像,用于以解耦的方式管理 Web 应用程序、数据库和内存缓存。
将每个容器限制为一个进程是一个不错的经验法则,但并非一成不变。例如,不仅可以通过 init 进程生成容器,某些程序还可以自行生成其他进程。例如,Celery 可以生成多个工作进程,而 Apache 可以为每个请求创建一个进程。
请尽最大努力保持容器的简洁和模块化。如果容器相互依赖,您可以使用 Docker 容器网络来确保这些容器能够通信。
11. Dockerfile 的最佳实践
RUN
pipeline
某些 RUN 命令依赖于使用管道符 (|) 将一个命令的输出通过管道传输到另一个命令的能力,如下例所示:
RUN wget -O - https://some.site | wc -l > /number
Docker 使用 /bin/sh -c 解释器执行这些命令,该解释器仅评估管道中最后一个操作的退出代码来确定是否成功。在上面的示例中,只要 wc -l 命令成功,即使 wget 命令失败,此构建步骤也会成功并生成新的镜像。
如果您希望该命令因管道中任何阶段的错误而失败,请在命令前面添加 set -o pipefail && ,以确保意外错误会阻止构建意外成功。例如:
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
注意
并非所有 Shell 都支持 -o pipefail 选项。
对于基于 Debian 的镜像上的 dash shell 等情况,请考虑使用 RUN 的 exec 形式明确选择支持 pipefail 选项的 shell。例如:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
apt-get
在基于 Debian 的镜像中,RUN 指令的一个常见用例是使用 apt-get 安装软件。由于 apt-get 会安装软件包,因此 RUN apt-get 命令存在一些需要注意的违反直觉的行为。
请务必在同一个 RUN 语句中结合使用 RUN apt-get update 和 apt-get install。例如:
RUN apt-get update && apt-get install -y --no-install-recommends \
package-bar \
package-baz \
package-foo
在 RUN 语句中单独使用 apt-get update 会导致缓存问题,并导致后续的 apt-get install 指令失败。例如,以下 Dockerfile 中会出现此问题:
# syntax=docker/dockerfile:1FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
构建镜像后,所有层都位于 Docker 缓存中。假设您稍后修改 apt-get install 命令,添加一个额外的软件包,如下 Dockerfile 所示:
# syntax=docker/dockerfile:1FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl nginx
Docker 会将初始指令和修改后的指令视为相同,并重用之前步骤的缓存。因此,apt-get update 命令不会执行,因为构建使用的是缓存版本。由于 apt-get update 命令未运行,您的构建可能会获得过时版本的 curl 和 nginx 软件包。
使用 RUN apt-get update && apt-get install -y --no-install-recommends 可确保您的 Dockerfile 安装最新的软件包版本,无需进一步编码或手动干预。此技术称为缓存清除。您也可以通过指定软件包版本来实现缓存清除。这称为版本锁定。例如:
RUN apt-get update && apt-get install -y --no-install-recommends \
package-bar \
package-baz \
package-foo=1.3.*
版本锁定会强制构建程序检索特定版本,而不管缓存中的内容。此技术还可以减少由于所需软件包发生意外更改而导致的故障。
以下是一条格式正确的 RUN 指令,演示了所有 apt-get 建议。
RUN apt-get update && apt-get install -y --no-install-recommends \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*
FROM
尽可能使用最新的官方镜像作为镜像的基础。Docker 推荐使用 Alpine 镜像,因为它控制严格且体积小巧(目前不到 6 MB),同时仍然是一个完整的 Linux 发行版。
LABEL
您可以为镜像添加标签,以便按项目组织镜像、记录许可信息、辅助自动化或用于其他目的。对于每个标签,添加一行以 LABEL 开头的文本,其中包含一个或多个键值对。以下示例展示了不同的可接受格式。解释性注释已内联。
带有空格的字符串必须用引号引起来或进行转义。内部引号 (") 也必须转义。例如:
# 设置一个或多个单独的标签
# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""
一个镜像可以有多个标签。在 Docker 1.10 之前,建议将所有标签合并到一个 LABEL 指令中,以防止创建额外的层。现在不再需要这样做,但仍然支持合并标签。例如:
# 在一行上设置多个标签
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
以上示例也可以写成:
# 一次设置多个标签,使用行继续符来断开长标签行
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
CMD
CMD 指令应该用于运行镜像中包含的软件,并附带任何参数。CMD 几乎总是以 CMD ["executable", "param1", "param2"] 的形式使用。因此,如果镜像用于服务(例如 Apache 和 Rails),则可以运行类似 CMD ["apache2","-DFOREGROUND"] 的指令。实际上,对于任何基于服务的镜像,都建议使用此形式的指令。
在大多数其他情况下,应该为 CMD 指定一个交互式 shell,例如 bash、python 和 perl。例如,CMD ["perl", "-de0"]、CMD ["python"] 或 CMD ["php", "-a"]。使用此形式意味着,当您执行类似 docker run -it python 的命令时,您将进入一个可用的 shell,随时可以运行。除非您和您的预期用户已经非常熟悉 ENTRYPOINT 的工作方式,否则 CMD 很少应以 CMD ["param", "param"] 的方式与 ENTRYPOINT 结合使用。
ENV
为了使新软件更容易运行,您可以使用 ENV 更新容器所安装软件的 PATH 环境变量。例如,ENV PATH=/usr/local/nginx/bin:$PATH 可确保 CMD ["nginx"] 正常运行。
ENV 指令还可用于提供特定于您要容器化的服务(例如 Postgres 的 PGDATA)所需的环境变量。
最后,ENV 还可用于设置常用的版本号,以便更轻松地维护版本升级,如下例所示:
ENV PG_MAJOR=9.3
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH
与在程序中使用常量变量(而非硬编码值)类似,这种方法允许您更改单个 ENV 指令,以自动升级容器中软件的版本。
每行 ENV 指令都会创建一个新的中间层,就像 RUN 命令一样。这意味着即使您在后续层中取消设置环境变量,它仍然会保留在此层中,并且其值可以被转储。您可以通过创建如下所示的 Dockerfile 并进行构建来测试这一点。
# syntax=docker/dockerfile:1
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USERdocker run --rm test sh -c 'echo $ADMIN_USER'mark
为了防止这种情况,并真正地取消设置环境变量,请在 shell 命令中使用 RUN 命令,在同一个层级中设置、使用和取消设置变量。您可以使用 ; 或 && 分隔命令。如果您使用第二种方法,并且其中一个命令失败,docker 构建也会失败。这通常是一个好主意。在 Linux Dockerfile 中使用 \ 作为行继续符可以提高可读性。您也可以将所有命令放入一个 shell 脚本中,并让 RUN 命令直接运行该 shell 脚本。
# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USER
CMD shdocker run --rm test sh -c 'echo $ADMIN_USER'
USER
如果服务无需特权即可运行,请使用 USER 切换到非 root 用户。首先,在 Dockerfile 中创建用户和组,例如:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
注意
请考虑使用明确的 UID/GID。
镜像中的用户和组会被分配一个非确定性的 UID/GID,因为无论镜像是否重建,都会分配“下一个”UID/GID。因此,如果情况紧急,您应该分配一个明确的 UID/GID。
注意
由于 Go 归档/tar 包在处理稀疏文件时存在一个尚未解决的 bug,尝试在 Docker 容器内创建具有较大 UID 的用户可能会导致磁盘空间耗尽,因为容器层中的 /var/log/faillog 文件会被 NULL (\0) 字符填充。一种解决方法是将 --no-log-init 标志传递给 useradd。Debian/Ubuntu adduser 包装器不支持此标志。
避免安装或使用 sudo,因为它具有不可预测的 TTY 和信号转发行为,可能会导致问题。如果您确实需要类似于 sudo 的功能,例如以 root 身份初始化守护进程但以非 root 身份运行它,请考虑使用“gosu”。
最后,为了减少层级和复杂性,请避免频繁来回切换 USER 角色。
总结
本文探讨了 Docker 多阶段构建的强大功能,该功能可帮助开发者优化镜像大小并提高构建效率。多阶段构建支持构建环境和运行时环境的分离,允许在一个阶段编译应用程序,并在另一个阶段将其打包成最终镜像。这不仅可以通过排除不必要的构建依赖项来减小镜像整体大小,还能增强最终镜像的安全性。
本文深入探讨了有效的缓存设置,解释了 Docker 的缓存机制如何显著加快构建过程。通过策略性地利用层和缓存清除,开发者可以最大限度地缩短重建时间并优化工作流程。
此外,本文还提供了使用多阶段构建的实用技巧,例如利用 .dockerignore 文件从构建上下文中排除不必要的文件,高效组织 Dockerfile 指令以最大化层缓存,以及保持构建过程的清晰度以便于调试和维护。
总的来说,本文为希望充分利用 Docker 多阶段构建潜力的开发人员提供了全面的指南,并提供了可行的策略来增强他们的容器化流程。