问题描述
我正在使用类似Dockerfile的多阶段构建:
#####################################
## Build the client
#####################################
FROM node:12.19.0 as web-client-builder
workdir /workspace
copY web-client/package*.json ./
# Running npm install before we update our source allows us to take advantage
# of docker layer caching. We are excluding node_modules in .dockerignore
RUN npm ci
copY web-client/ ./
RUN npm run test:ci
RUN npm run build
#####################################
## Host the client on a static server
#####################################
FROM Nginx:1.19 as web-client
copY --from=web-client-builder /workspace/Nginx-templates /etc/Nginx/templates/
copY --from=web-client-builder /workspace/Nginx.conf /etc/Nginx/Nginx.conf
copY --from=web-client-builder /workspace/build /var/www/
#####################################
## Build the server
#####################################
FROM openjdk:11-jdk-slim as server-builder
workdir /workspace
copY build.gradle settings.gradle gradlew ./
copY gradle ./gradle
copY server/ ./server/
RUN ./gradlew --no-daemon :server:build
#####################################
## Start the server
#####################################
FROM openjdk:11-jdk-slim as server
workdir /app
ARG JAR_FILE=build/libs/*.jar
copY --from=server-builder /workspace/server/$JAR_FILE ./app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]
我也有一个像这样的docker-compose.yml:
version: "3.8"
services:
server:
restart: always
container_name: server
build:
context: .
dockerfile: Dockerfile
target: server
image: server
ports:
- "8090:8080"
web-client:
restart: always
container_name: web-client
build:
context: .
dockerfile: Dockerfile
target: web-client
image: web-client
environment:
- LISTEN_PORT=80
ports:
- "8091:80"
此处涉及的两个图像web-client
和server
是完全独立的。我想利用多阶段构建并行化的优势。
当我运行docker-compose build
(我在docker-compose 1.27.4上)时,我得到这样的输出
λ docker-compose build
Building server
Step 1/24 : FROM node:12.19.0 as web-client-builder
---> 1f560ce4ce7e
... etc ...
Step 6/24 : RUN npm run test:ci
---> Running in e9189b2bff1d
... Runs tests ...
... etc ...
Step 24/24 : ENTRYPOINT ["java","/app/app.jar"]
---> Using cache
---> 2ebe48e3b06e
Successfully built 2ebe48e3b06e
Successfully tagged server:latest
Building web-client
Step 1/11 : FROM node:12.19.0 as web-client-builder
---> 1f560ce4ce7e
... etc ...
Step 6/11 : RUN npm run test:ci
---> Using cache
---> 0f205b9549e0
... etc ...
Step 11/11 : copY --from=web-client-builder /workspace/build /var/www/
---> Using cache
---> 31c4eac8c06e
Successfully built 31c4eac8c06e
Successfully tagged web-client:latest
请注意,我的测试(npm run test:ci
)运行了两次(对于服务器目标,执行步骤6/24,对于Web客户端目标,则再次执行步骤6/11)。我想了解为什么会这样,但是我想这不是一个大问题,因为至少在第二次进行测试时就已对其进行了缓存。
这是一个更大的问题,当我尝试并行运行构建时。现在我得到这样的输出:
λ docker-compose build --parallel
Building server ...
Building web-client ...
Building server
Building web-client
Step 1/11 : FROM node:12.19.0 as web-client-builderStep 1/24 : FROM node:12.19.0 as web-client-builder
---> 1f560ce4ce7e
... etc ...
Step 6/24 : RUN npm run test:ci
---> e96afb9c14bf
Step 6/11 : RUN npm run test:ci
---> Running in c17deba3c318
---> Running in 9b0faf487a7d
> web-client@0.1.0 test:ci /workspace
> react-scripts test --ci --coverage --reporters=default --reporters=jest-junit --watchAll=false
> web-client@0.1.0 test:ci /workspace
> react-scripts test --ci --coverage --reporters=default --reporters=jest-junit --watchAll=false
... Now my tests run in parallel twice,and the output is interleaved for both parallel runs ...
很明显,这些测试现在已经运行了两次,因为既然我正在并行运行这些构建,那么就没有机会对其进行缓存。
有人可以帮助我理解吗?我认为Docker多阶段构建的最高点之一是它们是可并行化的,但是这种行为对我来说没有意义。我有什么误会?
注意 我还尝试过为docker-compose启用BuildKit。我很难理解输出。我不相信它会运行两次,但是我也不确定它是否并行化。我需要对其进行更多的研究,但是我的主要问题仍然是:我希望理解为什么多阶段构建不能以我期望的没有 BuildKit的方式并行运行。
解决方法
为什么在没有BuildKit的情况下,多阶段构建无法按我期望的方式并行运行。
这是BuildKit的重点。
Docker中多阶段的主要目的是通过仅保留应用程序正常运行所需的内容来生成较小的映像。例如
FROM node as builder
COPY package.json package-lock.json
RUN npm ci
COPY . /app
RUN npm run build
FROM nginx
COPY --from=/app/dist --chown=nginx /app/dist /var/www
构建项目所需的所有开发工具都不会完全复制到最终映像中。这将转化为较小的最终图像。
编辑:
BuildKit构建基于称为LLB的二进制中间格式,用于为运行部分构建的进程定义依赖关系图。 tl; dr:LLLB对Dockerfile而言就像LLVM IR对C一样。
换句话说,BuildKit能够评估允许并行执行的每个阶段的依赖性。
,您可以将其拆分为两个单独的Dockerfile。我可能会写一个包含前两个阶段的web-client/Dockerfile
(将相对的COPY
路径更改为./
),并留下根目录Dockerfile
来构建服务器应用程序。然后,您的docker-compose.yml
文件可以指向这些单独的目录:
services:
server:
build: . # equivalent to {context: .,dockerfile: Dockerfile}
web-client:
build: web-client
@Stefano在their answer中指出,多阶段构建围绕构建单个最终图像进行了更优化,在“经典”构建器中,它们始终从头开始运行到指定的目标阶段,没有任何特殊要求从哪里开始的逻辑。