多阶段构建深入
第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
安全加固清单
- 使用非 root 用户运行容器进程
- 使用 BuildKit Secret 传递密钥,不用 ARG/ENV
- 设置
--read-only只读文件系统 - 使用资源限制防止 DoS
- 使用
--cap-drop ALL移除不必要的 Linux Capabilities - 定期扫描镜像漏洞(docker scout / Trivy)
- 使用镜像签名验证供应链完整性