路线图

14-容器镜像构建

星辉 2026-07-02 阅读 4 min 652 字 路线图
14-容器镜像构建 封面

K8s 路线已经覆盖了容器基础(Pod、Image、Dockerfile 基本写法)。本章侧重构建优化——怎么让镜像更小、构建更快、更安全。三块内容:多阶段构建、层缓存与瘦身、构建工具对比、镜像扫描与标签策略。

多阶段构建#

核心思想:构建时需要的工具,运行时不需要。编译器、测试框架、调试工具统统留在构建阶段,最终镜像只包含运行时产物。

Go 应用示例#

Go 的静态编译特性使其成为多阶段构建的理想场景,最终可以用 scratch 或 distroless:

dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS deps
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go mod download

FROM deps AS builder
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -trimpath -o /app/server ./cmd/server

# 最终镜像用 distroless
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

关键优化:

  • -ldflags="-w -s" 去除调试符号,减小二进制体积约 30%
  • -trimpath 移除构建路径信息,提高可重现性
  • --mount=type=cache 复用 Go 模块缓存和编译缓存
  • distroless/static:nonroot 内置非 root 用户,无 shell、无包管理器,攻击面极小

Python 应用示例#

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS deps
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --prefix=/install -r requirements.txt

FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=deps /install /usr/local
COPY src/ ./src/
RUN groupadd --gid 1000 appuser && \
    useradd --uid 1000 --gid appuser --no-create-home appuser
USER appuser
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]

层缓存与瘦身#

依赖层与代码层分离#

最重要的缓存优化原则——变更频率从低到高排列层:

dockerfile
# 错误:代码变更会导致依赖重新安装
COPY . .
RUN npm ci

# 正确:依赖声明文件不变则复用缓存
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/

层顺序:

  1. 基础镜像(FROM)
  2. 系统依赖安装(apt/apk)
  3. 应用依赖声明文件(go.mod、package.json)
  4. 应用依赖安装(go mod download、npm ci)
  5. 源代码(COPY . .)
  6. 构建步骤(RUN go build)

BuildKit cache mount#

--mount=type=cache 让包管理器缓存跨构建持久化,不进镜像层:

dockerfile
# Go
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build ./...

# npm
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline

# pip
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Registry cache(跨 Runner 共享)#

CI 环境中本地缓存无法跨 Runner 共享,用 registry cache:

bash
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:cache \
  --cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max \
  --tag registry.example.com/myapp:latest \
  --push .

mode=max 把所有中间层缓存(不只是最终阶段)都推到 registry,多阶段构建的命中率提升显著。

基础镜像选型#

基础镜像压缩大小Shell适用场景
ubuntu:24.04~30MB✓需要完整工具链
debian:bookworm-slim~30MB✓需要 glibc
alpine:3.19~3.5MBash节点代理、工具类
gcr.io/distroless/static~2MB✗Go 静态二进制
gcr.io/distroless/base~20MB✗需要 glibc
scratch0MB✗完全静态二进制

Distroless vs Alpine:

  • Alpine 用 musl libc,与 glibc 有兼容性问题(如 numpy 等 C 扩展需要重编译)
  • Distroless 基于 Debian,用 glibc,兼容性更好
  • Go 应用优先 distroless/static:nonroot;Python/Node 用对应语言的 distroless 变体

构建工具对比#

工具工作方式特权要求适用场景
docker build传统 Docker daemon 构建需要 daemon本地开发、DinD(不推荐)
BuildKitDocker 23.0+ 默认引擎(18.09 引入),并行+精细缓存需要 daemon 或 rootless现代 docker build 默认;rootless 适合 K8s CI
BuildahRed Hat 出品,无 daemon不需要特权K8s CI 环境(现代推荐)
Kaniko用户态构建,无 daemon不需要特权K8s CI(Google 已于 2025-06 归档,见下)

Kaniko:曾经的 K8s CI 首选(2025-06 已归档)#

Kaniko 在用户态完成镜像构建,不需要 Docker daemon、不需要特权容器:

yaml
# GitLab CI
build-image:
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  script:
    - /kaniko/executor
        --context $CI_PROJECT_DIR
        --dockerfile $CI_PROJECT_DIR/Dockerfile
        --destination $IMAGE_NAME:$IMAGE_TAG
        --cache=true
        --cache-repo=$IMAGE_NAME/cache

Kaniko 的优势:

  • 无需特权容器,K8s 多租户环境安全友好
  • 不需要 DinD(Docker-in-Docker)
  • 支持和 docker build 一样的 Dockerfile 语法
  • 内置 registry cache 支持

注意(2026):Google 已于 2025-06 归档 kaniko 仓库(只读、不再新增功能),Chainguard 接手仅做维护与安全补丁,GitLab 也已移除官方 kaniko 构建文档。存量 kaniko 可继续用,但新流水线优先考虑 BuildKit rootless 或 Buildah——同样无需特权、不需要 DinD,且仍在活跃维护。

镜像扫描#

构建出镜像后扫描漏洞:

bash
# Trivy 扫描镜像
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:latest

# 输出 SARIF 格式给 GitHub Security tab
trivy image --format sarif --output trivy-results.sarif myapp:latest

CI 集成:

yaml
# GitHub Actions
- name: Run Trivy scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: sarif
    output: trivy-results.sarif
    severity: CRITICAL,HIGH
    exit-code: '1'    # 有高危漏洞则失败

- name: Upload to GitHub Security
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy-results.sarif

或集成到镜像仓库(Harbor)——push 后自动扫描,详见 15 章。

标签策略#

绝不用 latest#

latest 是反模式:

  • 不可溯源:latest 今天的内容和昨天的不一样,出问题不知道回滚到哪
  • 不可回滚:回滚需要"上一个版本",latest 没有版本概念
  • 缓存陷阱:K8s 默认 imagePullPolicy: IfNotPresent,latest 会变成 Always,每次都拉镜像

推荐 tag 格式#

text
{commit短hash}-{YYYY-MM-DD-HH-MM-SS}

例:abc1234-2026-06-09-10-30

  • 可溯源:从 tag 能看到 commit hash 和构建时间
  • 全环境复用:同一镜像从 QA → PRE → PROD,不重新构建
  • 回滚友好:回滚就是切到上一个 tag
bash
# CI 里这样打 tag
TAG="${CI_COMMIT_SHORT_SHA}-$(date +%Y-%m-%d-%H-%M-%S)"
docker build -t myapp:${TAG} .
docker push myapp:${TAG}

其他 tag 策略#

  • Git tag 发版:v1.2.3 这种 SemVer tag 用于正式发布
  • 分支名 tag:main develop 用于跟踪分支最新(辅助,不用于生产)
  • latest 限定场景:开源项目的"最新稳定版"指示器,配合 SemVer tag 使用

优化效果对比#

以一个典型 Go Web 服务为例:

指标优化前(ubuntu + 完整 Go 工具链)优化后(distroless + 多阶段 + 缓存)改善
镜像大小892MB18MB-98%
冷构建时间4m32s3m15s-28%
热构建(只改代码)4m32s0m48s-82%
高危 CVE 数230-100%
拉取时间8.2s0.3s-96%

实战要点#

  • 多阶段构建是镜像优化的基础。所有生产镜像都应该是多阶段,最终镜像不含构建工具链。
  • 依赖层和代码层分离。这条规则的影响最大——改代码不重新装依赖,热构建时间能降一个数量级。
  • BuildKit cache mount + registry cache 双重缓存。前者加速单机多次构建,后者加速跨 Runner 构建。
  • K8s CI 构建优先 BuildKit rootless / Buildah。三者都无需特权、不需要 DinD;kaniko 曾是首选,但 Google 已于 2025-06 归档,新项目慎用。
  • 绝不用 latest。用 {commit-hash}-{timestamp} 格式,可溯源、可回滚。
  • distroless 优于 alpine。alpine 的 musl libc 兼容性问题踩坑成本高,distroless 用 glibc 更稳。
  • 镜像扫描进 CI。构建后立即扫描,高危漏洞阻塞部署。

小结#

容器镜像构建优化是一个全链路工程:多阶段构建 + 依赖层分离解决镜像大小、BuildKit cache + registry cache 解决构建速度、distroless 基础镜像 + Trivy 扫描解决安全、{commit}-{timestamp} 标签解决可溯源。

建议按"先解决速度(缓存)→ 再解决大小(多阶段+基础镜像)→ 最后解决安全(扫描+签名)"的顺序推进,每一步都有明确可量化的收益。下一章讲镜像仓库怎么选。