docker 差异化打包方式

Docker

在使用docker build命令的时候,有时候需要根据不同的规则指定不同的构建脚本,比如 dockerfile分为prod或test,或者将 变量传到dockerfile中根据不同的条件执行不同的构建命令; 还比如在不同的打包环境下可能构建出的产物目录不一样,有的是dist或dist/app1或dist/app2, 那么有哪些方式可以灵活的实现这些差异化构建呢?

🌙 几种实现差异化构建的主流方式

🌙 方式一:使用不同的 Dockerfile 文件 (-f 参数)

这是最直接、最简单的方式。你可以为不同的环境创建不同的Dockerfile。

  • Dockerfile.prod (生产环境)
  • Dockerfile.dev (开发环境)
  • Dockerfile.test (测试环境)

工作原理:docker build 命令中使用 -f--file 参数来明确指定使用哪个构建脚本。

示例:

Dockerfile.prod:

# === 生产环境构建脚本 ===
# Stage 1: 构建前端应用
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 生产环境构建命令
RUN npm run build --prod

# Stage 2: 部署到 Nginx
FROM nginx:stable-alpine
# 从构建阶段拷贝产物,假设产物在 dist 目录
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Dockerfile.test:

# === 测试环境构建脚本 ===
# Stage 1: 构建前端应用
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 测试环境构建命令,可能包含测试工具
RUN npm run build:test

# Stage 2: 部署到 Nginx
FROM nginx:stable-alpine
# 从构建阶段拷贝产物,假设产物在 test-dist 目录
COPY --from=builder /app/test-dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

构建命令:

# 构建生产镜像
docker build -t my-app:prod -f Dockerfile.prod .

# 构建测试镜像
docker build -t my-app:test -f Dockerfile.test .
1
2
3
4
5
  • 优点:
    • 完全隔离:每个环境的构建逻辑完全独立,互不干扰。
    • 清晰易懂:对于差异巨大的构建流程,这种方式最直观。
  • 缺点:
    • 代码冗余:如果两个文件大部分内容都相同(比如基础镜像、依赖安装步骤),维护起来很麻烦。修改一个公共部分,需要同步修改所有文件,容易出错。
  • 适用场景:当不同环境的构建逻辑差异非常大时(例如,基础镜像都不同,或者构建流程完全不一样)。

🌙 方式二:使用构建参数 (ARG--build-arg)

这是最灵活、最推荐的方式,尤其适合你提到的场景。它允许你将变量从 docker build 命令传递到 Dockerfile 内部,从而实现条件判断。

工作原理:

  1. 在 Dockerfile 中使用 ARG 指令定义一个构建时变量。
  2. docker build 命令中使用 --build-arg 来传递这个变量的值。
  3. 在 Dockerfile 的 RUN, COPY 等指令中使用这个变量。

示例1:根据变量执行不同的构建命令

Dockerfile (单一文件):

FROM node:18-alpine AS builder
WORKDIR /app

# 定义一个构建环境的参数,默认值为 "prod"
ARG BUILD_ENV=prod

COPY package*.json ./
RUN npm install
COPY . .

# 根据 BUILD_ENV 的值执行不同的命令
RUN if [ "$BUILD_ENV" = "prod" ]; then \
      echo ">>> Running production build" && npm run build; \
    else \
      echo ">>> Running test build" && npm run build:test; \
    fi

# ...后续阶段...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

构建命令:

# 构建生产镜像 (使用默认 ARG 值)
docker build -t my-app:prod .

# 构建测试镜像 (传递不同的 ARG 值)
docker build --build-arg BUILD_ENV=test -t my-app:test .
1
2
3
4
5

示例2:处理不同的产物目录 (dist, dist/app1, dist/app2)

Dockerfile (单一文件):

# Stage 1: 构建
FROM node:18-alpine AS builder
WORKDIR /app
# ... (npm install, etc.)
COPY . .
RUN npm run build # 假设这个命令会生成 dist/app1 或 dist/app2

# Stage 2: 部署
FROM nginx:stable-alpine

# 定义一个参数来指定资源路径,并给一个安全的默认值
ARG ASSET_PATH=dist

# 在 COPY 命令中使用这个变量
# 注意:$ASSET_PATH 后面必须有斜杠,表示拷贝目录内容
COPY --from=builder /app/${ASSET_PATH}/ /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

构建命令:

# 默认构建 (产物在 dist 目录)
docker build -t my-app:latest .

# 构建 app1 (产物在 dist/app1 目录)
docker build --build-arg ASSET_PATH=dist/app1 -t my-app:app1 .

# 构建 app2 (产物在 dist/app2 目录)
docker build --build-arg ASSET_PATH=dist/app2 -t my-app:app2 .
1
2
3
4
5
6
7
8
  • 优点:
    • 单一文件:维护成本低,所有逻辑集中在一个地方。
    • 高度灵活:非常适合CI/CD流水线,只需改变传入的参数即可。
    • 避免冗余:所有环境共享公共的构建步骤。
  • 缺点:
    • Dockerfile 内部可能会出现一些 if/else 逻辑,稍微增加了一点复杂度。
  • 适用场景:绝大多数需要差异化构建的场景,是事实上的标准做法。

🌙 方式三:利用多阶段构建的 target

多阶段构建不仅可以减小镜像体积,还可以用来分离不同的构建目标。

工作原理: 你可以定义多个构建阶段,比如 builder, tester, production。然后在 docker build 时使用 --target 参数来指定构建到哪一个阶段为止。

示例:分离测试和构建

Dockerfile:

# ---- 基础构建阶段 ----
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

# ---- 测试阶段 ----
FROM builder AS tester
# 运行测试,如果测试失败,此阶段会失败
RUN npm test

# ---- 生产镜像阶段 ----
FROM builder AS release-builder
# 运行生产构建
RUN npm run build

# ---- 最终的生产镜像 ----
FROM nginx:stable-alpine AS production
COPY --from=release-builder /app/dist/ /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

构建命令:

# 1. 只运行测试,不生成最终镜像
# 如果 npm test 失败,整个构建就会失败,非常适合在 CI 中做代码检查
docker build --target tester -t my-app:test-runner .

# 2. 构建最终的生产镜像
# 这会自动执行依赖的阶段 (builder -> release-builder -> production)
docker build --target production -t my-app:latest .
1
2
3
4
5
6
7
  • 优点:
    • 逻辑清晰:将测试、构建、部署等不同关注点分离到不同的阶段。
    • CI/CD 友好:可以先构建 tester 阶段来验证代码,通过后再构建 production 阶段。
  • 缺点:
    • 它主要解决的是构建流程的分离,对于条件化拷贝文件等细粒度控制,仍需结合方式二 (ARG) 来实现。
  • 适用场景:希望在同一个 Dockerfile 中定义单元测试、集成测试、生产构建等多个独立目标。

🌙 最佳实践与总结

推荐组合使用 方式二 (ARG) 和多阶段构建

乃是当前最现代化、最灵活、最易于维护的方案。

方案 优点 缺点 最佳适用场景
1. 多文件 (-f) 完全隔离,逻辑清晰 代码冗余,维护困难 环境差异巨大,几乎无公共部分时
2. 构建参数 (ARG) 灵活无冗余CI/CD友好 需在 RUN 中写少量shell逻辑 95%的差异化构建场景
3. 多阶段目标 (--target) 流程分离,适合 CI 门禁 不直接解决条件判断问题 定义测试、构建等不同构建目标

🌙 最终建议方案 (综合示例)

在一个名为 Dockerfile 的文件中,结合多阶段构建和 ARG 参数:

# =========================================================
# Stage 1: Builder - 通用的构建环境
# =========================================================
FROM node:18-alpine AS builder
WORKDIR /app

# --- ARG 定义区 ---
# 定义构建产物的路径,默认是 'dist'
ARG ASSET_PATH=dist
# 定义构建脚本命令,默认是 'build'
ARG BUILD_SCRIPT=build

# 打印出接收到的参数,便于调试
RUN echo ">>> Building with ASSET_PATH=${ASSET_PATH} and SCRIPT=${BUILD_SCRIPT}"

COPY package*.json ./
RUN npm install
COPY . .

# 执行由参数指定的构建脚本
RUN npm run ${BUILD_SCRIPT}

# =========================================================
# Stage 2: Production - 最终的生产镜像
# =========================================================
FROM nginx:stable-alpine
# ARG 必须在每个阶段重新声明才能使用
ARG ASSET_PATH=dist

# 从 builder 阶段拷贝由 ASSET_PATH 指定的目录内容
COPY --from=builder /app/${ASSET_PATH}/ /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这样,你的 CI/CD 流水线就可以极其灵活地构建:

# 场景A: 标准生产构建 (产物在 dist)
docker build -t my-app:1.0.0 .

# 场景B: 为项目 app1 构建 (产物在 project/app1, 使用 build:app1 脚本)
docker build \
  --build-arg ASSET_PATH=project/app1 \
  --build-arg BUILD_SCRIPT=build:app1 \
  -t my-app-app1:1.0.0 .

# 场景C: 为项目 app2 构建 (产物在 project/app2, 使用 build:app2 脚本)
docker build \
  --build-arg ASSET_PATH=project/app2 \
  --build-arg BUILD_SCRIPT=build:app2 \
  -t my-app-app2:1.0.0 .
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这个方案将 Dockerfile 的维护成本降到最低,同时将灵活性发挥到了极致,完全符合自动化运维对健壮性和可维护性的要求。