为 Code Engine 编写 Dockerfile
在将代码构建到容器映像中之前,请了解 Docker 构建如何在 IBM Cloud® Code Engine中工作的一些基础知识。 然后,查看 Dockerfile 的一些最佳实践以实现这些目标。 这些示例侧重于减小映像大小,提高与 Code Engine 应用程序的互操作性,以及以非 root 用户身份运行容器。
创建构建配置时,您将决定要使用两种可用策略中的哪一种。
-
Cloud Native Buildpack 会检查源代码,并检测代码所基于的运行时环境以及从源构建容器映像的方式。 它受多个编程环境支持。 您可以在 选择构建策略 下找到受支持的列表。
-
Docker 构建根据您在 Dockerfile 中描述容器的方式来创建容器。 然后,将 Dockerfile 与源代码一起落实以创建容器。
虽然您可以将任一策略用于构建,但可以选择 Dockerfile,例如,
- Buildpack 不支持您的编程环境。
- 您的项目构建必须在容器中安装其他软件包。
Dockerfile 基础知识
Dockerfile 描述如何构建容器。 在 Dockerfile 中,您可以选择基本映像,其中包含构建和运行时所需的必要工具。 您可以将文件从构建上下文复制到映像,运行命令,定义运行时行为 (例如,环境变量,公开的端口) 以及设置 ENTRYPOINT
。 ENTRYPOINT
由启动容器时调用的命令设置。 有关如何指定 Dockerfile 指示信息的更多信息,请参阅 Dockerfile 参考。
在 Code Engine 构建中,定义指向 Git 存储库的源。 缺省情况下,可用于 Docker 构建的上下文是 Git 存储库的根目录。 例如,如果存储库中有一个名为 src
的目录,那么可以使用 Dockerfile 中的 COPY
语句将此目录复制到映像中。 例如
COPY src /app/src
要复制整个 Git 存储库,请指定以下 COPY
语句:
COPY . /app/src
如果复制整个 Git 存储库,但想要排除某些文件 (例如存储库的 README.md
),那么可以添加 .dockerignore
文件。 使用同一文件还可忽略您在 .gitignore 文件中指定的文件和目录。 通过使用同一文件,确保在本地运行的构建具有与在 Code Engine中运行的构建相同的一组可用文件。
始终将应用程序文件复制到根目录 (/
) 的子目录中,而不是直接复制到根目录中,以避免与操作系统文件发生冲突。 命名应用程序目录时,请勿使用基于 Unix 的操作系统,Kubernetes 或 Code Engine 构建 (例如 /bin
,/dev
,/etc
,/lib
,/proc
,/run
,/sys
,/usr
,/var
或 /workspace
) 保留的应用程序目录。 最佳实践是对应用程序目录 /app
进行命名。
如果源代码存储库包含在目录中组织的不同应用程序的源,类似于 Code Engine 样本存储库,那么可以使用子目录作为上下文。 在 ibmcloud ce build create
命令中,使用 --context-dir
选项指定子目录。
如果 Dockerfile 不在上下文目录中,那么可以使用 --dockerfile
参数指向该文件。
要在本地系统上试用 Docker 构建,然后在 Code Engine中进行构建,可以使用 Docker Desktop。
减小容器映像的大小
减小容器映像的大小会带来多个方面的值。
- 在容器注册表中存储映像所需的空间更少。 您可以为其他映像节省配额并节省资金。
- 有时,构建运行需要更少的时间来完成,因为与更大的映像相比,较小的映像可以更快地传输到容器注册表。 你又省钱了。
- 使用该映像的应用程序或作业启动速度更快,因为拉取该映像所需的时间更短。 由于在拉取映像时保留了运行应用程序或作业所需的资源,因此再次节省了资金。 快速启动时间特别适用于 Code Engine 中的应用程序,因为它保证用户的请求具有可接受的响应时间,即使应用程序缩减为零也是如此。
请查看以下一些最佳实践,以减少构建的大小。
在单个 RUN
语句中组合多个命令以减小映像大小
在此示例中,必须在容器映像中安装软件,例如 Node.js。使用 Node.js 的基本映像来构建 Node.js 应用程序。
要手动安装 Node.js,可以使用以下 Dockerfile 示例:
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y nodejs
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
在此示例中,多个 RUN
语句用于更新,升级,安装 Node.js,清除和除去文件的高速缓存。 虽然此语句序列是正确的,但使用多个 RUN
语句并不是实现该语句的最佳方法。 处理 Dockerfile 时,Dockerfile 中的每个语句都会创建一个层。 每个层都包含已创建或更新的文件以及有关已删除内容的信息。 容器映像是所有层的集合。 如果在一个语句中添加了文件并在第二个语句中删除了文件,那么生成的图像仍包含第一个语句的层中的文件,然后在第二个层中将其标记为已删除。
要节省磁盘空间,请指定单个 RUN
语句来为此组命令创建单层。
FROM ubuntu
RUN apt update && apt upgrade -y && apt install -y nodejs && apt clean && rm -rf /var/lib/apt/lists/\*
要维护可读的 Dockerfile,每个命令一行,请向代码添加换行符,
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
通过使用此类型的语句,可以将容器映像大小从大约 174 MB 减少到大约 147 MB。 请注意,您的示例大小可能有所不同,因为 Ubuntu 基本映像和 Node.js 包可能会发生更改。
此类型的最佳实践不仅适用于软件包安装,还适用于类似的任务。 在下一个示例中,下载和解压缩软件包 (同样是 Node.js) 可能类似于以下示例:
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y curl
RUN curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz
RUN mkdir /opt/node
RUN tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1
RUN rm /tmp/nodejs.tar.gz
RUN apt remove -y curl
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
rm
命令将除去已下载的 nodejs
包,但多个 RUN
语句将再次创建单独的层。 此外,要下载 nodejs
软件包,请临时安装 curl
软件包。 虽然 curl 软件包本身稍后会除去,但其隐式安装的依赖关系仍存在。 更好的 Dockerfile 类似于以下示例:
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y curl && \
curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz && \
mkdir /opt/node && \
tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1 && \
rm /tmp/nodejs.tar.gz && \
apt remove -y curl && \
apt auto-remove -y && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
相关命令将组合到单个 RUN
语句中,并添加 apt auto-remove
命令以除去依赖关系。 此示例将图像大小从大约 222 MB 减小到大约 162 MB。
使用极小的基本图像
以上示例使用 Ubuntu 作为基本映像。 虽然此映像包含许多有用的实用程序,但基本映像中的实用程序越多,其大小越大。 此外,通过包含更多实用程序,您可以增加迂到安全漏洞的机会,从而要求您重建映像。 要避免这两个问题,请使用较小的基本映像。 例如
-
Alpine 是具有较小大小的正式 Docker 映像。 对于 Java 或 Node.js之类的编程环境,您通常会找到基于 Alpine的标记。
-
来自 Google Container Tools 的无磁盘映像完全不包含操作系统工具,但不包含适用于不同语言 (例如 Java 和 Node.js) 的必需运行时环境。
通过在 program.js 文件中构建用于运行 Node.js 程序的容器映像来比较这些基本映像。 对于 Ubuntu,Dockerfile 类似于以下示例:
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
对于 Alpine,直接从 Node.js获取映像。
FROM node:16-alpine
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
对于 distroless,请使用以下示例:
FROM gcr.io/distroless/nodejs:16
COPY program.js /app/program.js
WORKDIR /app
CMD ["program.js"]
当基于 Ubuntu的映像为 147 MB 时,基于 Alpine 的映像为 90 MB,基于 distroless 的映像为 94 MB。
不包含用于减少图像大小的源和构建工具
在先前基于 Node.js 的示例中,将单个源文件添加到容器映像。 此示例可以使用单个源文件,因为不需要编译。 但是,如果需要编译,那么请仅对构建使用必需的工具,但不要在生成的映像中包含这些工具。 例如,指定使用 Maven 作为示例的 Java 应用程序。 编码不良的 Dockerfile 类似于以下示例:
FROM maven:3-jdk-11-openj9
WORKDIR /app
COPY . /app
RUN mvn package
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/target/java-application-1.0-SNAPSHOT-fat.jar"]
生成的容器映像包含来自 Maven 的所有源代码和所有中间文件 (例如其工件高速缓存,后来打包到 JAR 文件中的类文件) 以及 Maven 构建工具。 此外,在运行时需要更小的 Java 运行时环境 (JRE) 时,还会包含 Java Development Kit (JDK)。 因此,图像大小为 466 MB。
要使图像更小,请使用称为多阶段构建的功能。 Docker 构建中的每个阶段都有自己的基本映像,并且可以在其阶段中运行命令。 最后,将构建一个最终映像,该映像将在先前阶段的工件中进行复制。 要从源代码构建容器映像,公共模式有两个阶段:
- 使用基本映像的构建器阶段,其中包含将源代码编译为运行时的二进制文件所需的所有工具。
- 运行时阶段,它将基本映像与运行二进制文件所必需的运行时环境配合使用。 将来自构建器阶段的二进制文件复制到此阶段。
对于 Maven 项目,结果类似于以下示例:
FROM maven:3-jdk-11-openj9 AS builder
WORKDIR /app
COPY . /app
RUN mvn package
FROM adoptopenjdk:11-jre-openj9
WORKDIR /app
COPY --from=builder /app/target/java-application-1.0-SNAPSHOT-fat.jar /app/java-application.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/java-application.jar"]
在此示例中,构建器阶段使用 Maven 基本映像来构建应用程序的 JAR 文件。 运行时阶段使用较小的 JRE 基本映像。 通过使用 COPY --from=STAGE_NAME
命令,将 JAR 文件从构建器阶段复制到运行时阶段。 生成的图像大小为 242 MB,节省约 48%。
此模式还可用于需要执行编译的其他编程语言。
- 需要构建的 Node 应用程序,例如 Angular 或 React 构建。 在这里,构建器和运行时基本映像最终可能相同 (两个节点),但并非所有构建时工件和源都需要复制到运行时映像中。
- 将源代码编译为在没有运行时环境 (例如 Go 或 Rust) 的情况下运行的本机可执行文件的任何编程语言。
对于那些生成本机可执行文件且完全不需要运行时环境的语言,请将另一个 Docker 功能用于运行时阶段: 临时。 Scratch 可以用作 FROM
命令中的基础,但不是最终容器映像。 相反,它告诉 Docker 构建完全不使用基本映像。 如果没有来自基本映像的任何操作系统文件,那么结果映像可以仅包含单个文件: 从构建器阶段复制过来的二进制文件。 请注意,根据编程语言和代码,您可能需要对编译器选项进行进一步调整,因为二进制文件可能依赖于某些操作系统文件存在。
保持图像清洁
当您首先开发 Dockerfile 并调试其工作方式时,可以安装一些临时工具或应用无需在最终容器映像中的其他工作负载。 除去这些临时文件和工作负载可使您的映像保持干净,小型且更安全。
缩短映像的开始时间
为了实现最大效率,容器映像必须尽可能快地启动。 对于应用程序,相关度量值是 HTTP 端点可用并准备接受和处理入局 HTTP 请求所需的时间。 对于基于 Knative 的 Code Engine 应用程序,启动速度尤其重要。 此类应用程序可以配置为在没有流量的情况下缩减为零运行实例,从而无需消耗资源,也无需花费任何资金。 当请求进入时,将启动实例以处理该请求。 容器必须尽快启动以响应新请求。
要改进应用程序的启动,请调查应用程序的实现,并查看是否可以应用模式,例如:
- 并行化独立初始化工作,例如,建立与数据库的连接并读取配置文件以从环境变量与邮件服务器进行通信。
- 延迟应用程序启动不需要的初始化工作,而是在首次需要时执行这些工作。
此外,在实现使用 Angular,React 或 Vue 等框架的 Web 应用程序时,您还可以避免常见的陷阱。 其中每个框架都基于带有 NPM 的 Node.js,并包含一个命令行界面,可以轻松设置项目。 例如,使用 create-react-app
命令创建的 React 应用程序将设置包含一些预定义脚本的
package.json
文件。 其中一个脚本是 start
,它将 Web 服务器与 Web 应用程序一起显示。 您的 Dockerfile 可能类似于以下示例:
FROM nodejs:16-alpine
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "run", "start"]
当此类型的 Dockerfile 工作时,它不会像调用 npm run start
命令时那样快速编译然后启动应用程序。 对于超出小样本大小的应用程序,此延迟尤其明显。 正确的方法是在构建时编译应用程序,并仅在启动时提供服务,
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
您将再次看到构建器和运行时两阶段模式。 另请注意,更新后的样本使用不同的端口 8080
。 当此示例与任何其他端口配合使用时,8080
是 Code Engine 应用程序的缺省端口。 此外,通过使用已编译的构建,node_modules
中安装的所有源和工具都不会包含在最终容器映像中,这将减小其大小 281-97 MB。
以非 root 用户身份运行容器
精心设计的系统遵循最少特权的原则-应用程序或用户仅获得执行特定操作所需的那些特权。 在 Code Engine中,运行应用程序服务器或某些批处理逻辑,这通常不需要对系统进行管理访问。 因此,它不得以 root 用户身份在其容器中运行。 一个好的做法是使用定义的用户来设置容器映像,并以非 root 用户身份运行。 例如,根据我们先前的场景,
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
Dockerfile 使用 USER
命令来指定它要作为用户和组 1100 运行。 请注意,此命令不会在容器映像中隐式创建指定的用户和组。 通常,此结构是可接受的,但如果应用程序逻辑要求用户及其主目录存在,那么必须显式创建用户和组:
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve && \
addgroup nonroot --gid 1100 && \
adduser nonroot --ingroup nonroot --uid 1100 --home /home/nonroot --disabled-password
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
扩展了运行时阶段中的 RUN
命令,以调用 addgroup
和 adduser
命令来创建具有主目录的组和用户。