Chapter 08

多阶段构建与镜像安全

深入多阶段构建案例,掌握容器安全加固的核心实践

多阶段构建深入

第4章介绍了多阶段构建的概念,本章通过更多语言的实际案例加深理解。

案例一:Java Spring Boot 应用


# 阶段1:Maven 构建
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build

# 先复制 pom.xml,下载依赖(利用缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源码并构建
COPY src ./src
RUN mvn package -DskipTests -B

# 阶段2:JRE 运行时(比 JDK 小很多)
FROM eclipse-temurin:21-jre-alpine AS runtime

WORKDIR /app

# 创建非 root 用户
RUN addgroup -S spring && adduser -S spring -G spring
USER spring

# 仅复制构建产物
COPY --from=build /build/target/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

案例二:Python FastAPI 应用


# 阶段1:安装依赖(含编译工具)
FROM python:3.12-slim AS builder

WORKDIR /build

# 安装编译工具(某些包需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 阶段2:精简运行时
FROM python:3.12-slim

WORKDIR /app

# 只复制安装好的包(从 builder 的 --user 目录)
COPY --from=builder /root/.local /root/.local

COPY . .

# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

--target 指定构建阶段

在 CI 中可以只构建到某一阶段(如用于测试):


# 只构建到 builder 阶段(用于运行测试)
docker build --target builder -t myapp:test .
docker run --rm myapp:test npm test

# 构建最终生产镜像
docker build --target runtime -t myapp:latest .

非 Root 用户运行容器

为什么重要

容器内以 root 运行存在容器逃逸风险:如果攻击者利用漏洞突破容器隔离,他们将以 root 权限访问宿主机。使用非 root 用户是纵深防御的重要一环。

在 Dockerfile 中创建专用用户


# Alpine Linux:用 addgroup/adduser
RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 --ingroup nodejs nextjs

# 设置文件权限
COPY --chown=nextjs:nodejs . .

# 切换到非 root 用户
USER nextjs

---

# Debian/Ubuntu:用 useradd
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser

USER appuser

运行时指定用户(覆盖镜像设置)


# 以 UID 1000 运行
docker run --user 1000:1000 myapp

# 以 nobody 用户运行(最小权限)
docker run --user nobody myapp

Secret 管理

反例:用 ARG/ENV 存储密钥(危险!)


# ❌ 错误!密钥会出现在镜像历史中
ARG API_KEY
RUN curl -H "Authorization: Bearer $API_KEY" https://api.example.com/data > /data

# docker history myimage 会显示 ARG API_KEY=your-secret-key

BuildKit Secret(推荐)

BuildKit 提供的 --secret 机制,Secret 不会出现在任何镜像层中:


# Dockerfile:挂载 secret 文件(仅在 RUN 时可见)
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) \
    && curl -H "Authorization: Bearer $API_KEY" https://api.example.com > /data

# 构建时传入 Secret(不会进入镜像)
docker build --secret id=api_key,src=./secrets/api.key -t myapp .

Docker Compose 中的 secrets


services:
  api:
    image: myapi
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt   # 不提交到 git!

容器只读文件系统

将容器文件系统设为只读,防止恶意代码在容器内写入文件:


# 只读根文件系统 + 允许特定目录可写(tmpfs)
docker run \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  --tmpfs /var/log \
  myapp

# docker-compose 中
services:
  api:
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

资源限制


# 限制内存和 CPU
docker run \
  --memory "512m" \           # 硬限制 512MB,超出 OOM Kill
  --memory-reservation "256m" \ # 软限制(宿主机内存紧张时保证)
  --cpus "1.5" \               # 最多使用 1.5 个 CPU 核心
  --cpu-shares 512 \           # 相对权重(默认1024)
  myapp

# Compose 中的资源限制
services:
  api:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          memory: 256M

Linux Capabilities 最小化

Docker 默认给容器保留了部分 Linux Capabilities(能力),遵循最小权限原则:


# 移除所有能力,只保留绑定低端口所需的
docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  myapp

# --no-new-privileges 防止提权(禁止 setuid 二进制文件提升权限)

镜像签名:确保供应链安全

Docker Content Trust (DCT)


# 启用 DCT:只允许拉取签名镜像
export DOCKER_CONTENT_TRUST=1

# 签名并推送镜像
docker push myregistry/myapp:1.0

cosign(Sigstore 项目)


# 安装 cosign
brew install cosign

# 生成密钥对
cosign generate-key-pair

# 签名镜像(推送后签名)
cosign sign --key cosign.key myregistry/myapp:1.0

# 验证签名
cosign verify --key cosign.pub myregistry/myapp:1.0

安全加固清单