
Docker 镜像安全:多阶段构建、非 root 用户、镜像扫描与 Dockerfile 最佳实践
容器安全始于镜像。一个构建不当的 Docker 镜像可能使生产环境暴露于漏洞、权限提升和供应链攻击之下。本指南涵盖安全镜像开发的完整生命周期,从编写安全的 Dockerfile 到在 CI/CD 中自动化漏洞扫描。
最小攻击面原则
镜像中的每个包、二进制文件和库都是潜在的攻击向量。镜像越小,可能包含的漏洞就越少。
| 基础镜像 | 压缩后大小 | 说明 |
|---|---|---|
| ubuntu:24.04 | ~30 MB | 完整操作系统 |
| debian:bookworm-slim | ~25 MB | 精简操作系统 |
| alpine:3.19 | ~3 MB | 最小化 Linux |
| gcr.io/distroless/static | ~2 MB | 无 shell |
| scratch | 0 MB | 空基础镜像 |
Distroless 镜像仅包含应用程序及其运行时依赖,没有 shell、没有包管理器、没有攻击者可利用的工具。

多阶段构建
Node.js 示例
# 阶段 1:安装生产依赖
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 阶段 2:构建
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段 3:生产运行时(distroless)
FROM gcr.io/distroless/nodejs22-debian12 AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json .
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]
Go 示例(scratch 镜像)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o server ./cmd/server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]
生成的镜像大小约为 10 MB,而不是 300+ MB。
Python 示例
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links /wheels /wheels/*.whl \
&& rm -rf /wheels
COPY src/ ./src/
RUN useradd --uid 1001 --create-home appuser
USER appuser
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
以非 root 用户运行
默认情况下,Docker 容器以 root 用户运行。如果攻击者逃逸出容器,他们将拥有主机的 root 权限。
# Debian/Ubuntu
RUN groupadd --gid 1001 appgroup \
&& useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser
USER appuser
# Alpine
RUN addgroup -S -g 1001 appgroup \
&& adduser -S -u 1001 -G appgroup -h /app appuser
USER appuser
# 数字 UID(适用于 distroless)
USER 1001:1001

Kubernetes 安全上下文
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1001
capabilities:
drop:
- ALL
Dockerfile 安全加固
固定精确版本
# 错误:使用浮动标签
FROM node:latest
# 正确:固定摘要以实现完全可重现
FROM node:22-alpine@sha256:abc123...
# 或至少固定次版本
FROM node:22.4.0-alpine3.19
避免在层中泄露密钥
# 错误:密钥被写入镜像层
RUN curl -H "Authorization: Bearer ${API_TOKEN}" https://api.example.com/download
# 正确:使用 BuildKit 密钥(从不写入层)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=api_token \
curl -H "Authorization: Bearer $(cat /run/secrets/api_token)" \
https://api.example.com/download
使用 .dockerignore
.git
.env
*.env
node_modules
__pycache__
*.pyc
.pytest_cache
coverage/
dist/
secrets/
*.key
*.pem

COPY 与 ADD
# 错误:ADD 可以获取远程 URL 并自动解压归档
ADD https://example.com/package.tar.gz /tmp/
# 正确:COPY 仅复制本地文件
COPY package.tar.gz /tmp/
RUN tar xzf /tmp/package.tar.gz -C /app && rm /tmp/package.tar.gz
验证下载
RUN curl -fsSL https://example.com/tool-v1.2.3-linux-amd64.tar.gz \
-o /tmp/tool.tar.gz \
&& echo "abc123expectedhash /tmp/tool.tar.gz" | sha256sum -c - \
&& tar xzf /tmp/tool.tar.gz -C /usr/local/bin/ \
&& rm /tmp/tool.tar.gz
使用 Trivy 进行镜像扫描
# 扫描本地镜像
trivy image my-api:latest
# 如果发现 HIGH 或 CRITICAL 级别的 CVE,则 CI 失败
trivy image --exit-code 1 --severity HIGH,CRITICAL my-api:latest
# 在构建前扫描文件系统
trivy fs --security-checks vuln,config,secret .
在 GitHub Actions 中集成 Trivy
- name: Build image
run: docker build -t my-api:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: my-api:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
内容信任与签名
# 使用 cosign (Sigstore) 签名镜像
cosign sign --key cosign.key registry.example.com/my-api:v1.0.0
# 验证签名
cosign verify --key cosign.pub registry.example.com/my-api:v1.0.0
# 在 GitHub Actions 中无密钥签名
cosign sign registry.example.com/my-api:${{ github.sha }}
层优化
# 错误:分开的 RUN 命令创建额外层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 正确:合并相关命令
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
结论
Docker 镜像安全是一个分层学科。多阶段构建在大多数情况下将攻击面减少 80-90%。非 root 容器防止权限提升。CI 中的镜像扫描在已知 CVE 到达生产环境之前捕获它们。Dockerfile 最佳实践消除了常见陷阱,如泄露的密钥和未固定的依赖。系统地实施这些实践,您的容器将从第一天起达到生产级标准。