Docker 容器化最佳实践
生产容器的镜像分层、distroless 基础、多架构构建与供应链安全。
容器进阶
为什么
基于 node:20、root 用户、未扫描依赖的 1.5GB 镜像是待发生的安全事件。严谨容器化削减镜像 80%,通过 distroless 减少 90% CVE 面,并支持多架构(amd64+arm64)跨平台部署。
何时使用
应用于任何进生产的容器,尤其需 SBOM 和签名镜像的受监管行业(金融、医疗)。本地开发镜像跳过加固(用 docker-compose 覆盖)。
如何做
1. 层顺序 — COPY package*.json + RUN npm ci 在 COPY . . 前,跨代码变更缓存依赖
2. Distroless 基础 — gcr.io/distroless/nodejs20-debian12 无 shell、无包管理器,更小更安全
3. 非 root 用户 — 在 builder 创建 USER,runner 中设置;容器逃逸 ≠ host root
4. 多架构 — docker buildx build --platform linux/amd64,linux/arm64 --push(支持 Apple Silicon + Graviton)
5. SBOM — docker buildx build --sbom=true 生成 SPDX 清单,作为 OCI 工件附到镜像
6. 签名 — cosign sign --key cosign.key myregistry/app:v1(Sigstore,Kubernetes 可策略验证)
7. 扫描 — trivy image --severity HIGH,CRITICAL myapp:v1,CI 发现则失败;用 .trivyignore 接受风险
8. 运行时加固 — docker run --read-only --cap-drop ALL --security-opt no-new-privileges
示例
# Dockerfile — 加固,多架构就绪
# syntax=docker/dockerfile:1.6(支持 --sbom、--provenance)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts
FROM gcr.io/distroless/nodejs20-debian12 AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=nonroot:nonroot dist ./dist
COPY package*.json ./
USER nonroot:nonroot
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]
# .dockerignore — 保持构建上下文小
node_modules
.git
*.md
.env*
dist
coverage
# 构建 + 推多架构 + SBOM + 签名
# docker buildx create --use --name mybuilder
# docker buildx build \
# --platform linux/amd64,linux/arm64 \
# --sbom=true --provenance=true \
# -t myregistry/app:v1 --push .
# cosign sign --key cosign.key myregistry/app:v1
# trivy image --severity HIGH,CRITICAL --exit-code 1 myregistry/app:v1
# 运行时 — 只读 rootfs,弃所有 cap
# docker run --read-only --tmpfs /tmp \
# --cap-drop ALL --security-opt no-new-privileges \
# -p 3000:3000 myregistry/app:v1
常见问题
- ⚠Distroless 破坏调试 — 无 shell 可 exec,用 debug 变体或临时 sidecar
- ⚠多架构构建慢 3 倍 — amd64 主机上 QEMU 模拟 arm64 慢,用原生 arm64 builder
- ⚠cosign 验证未强制 — 镜像已签名但 K8s 也允许未签名,加 Kyverno/OPA 策略
- ⚠Trivy 在 distroless 失败 — 无包管理器查询,用 --scanners vuln 配 OS DB
最佳实践
- ✓按摘要固定基础镜像 — FROM node:20-alpine@sha256:abc... 而非 :20-alpine,防供应链替换
- ✓用 BuildKit 缓存挂载 — RUN --mount=type=cache,target=/root/.npm 快速重建
- ✓签名且集群准入验证 — Kyverno 策略部署时拒绝未签名镜像
- ✓.dockerignore 严格 — 排除测试夹具、.git、node_modules,上下文缩 10 倍
常见问题解答
多阶段构建——为什么和怎么做?
分离构建环境(含 devDependencies、编译器)和运行时镜像。阶段 1 构建,仅复制 dist/ 到阶段 2(FROM node:18-alpine)。最终镜像无源码或构建工具——更小更安全。
如何让密钥不进 Docker 镜像?
绝不 COPY .env 进镜像或 ARG 密钥——镜像层可见。用 Docker BuildKit secrets(--secret)或运行时挂载(-e 或 --env-file)。用 trivy 扫描验证无密钥泄漏。
为什么固定镜像版本而非用 :latest?
:latest 无警告变化——基础镜像更新时构建断裂。固定到特定摘要(node:18-alpine@sha256:abc...)实现可复现构建。在计划 PR 中有意更新固定。
如何优化 Docker 层缓存?
Dockerfile 命令从最少变到最多变排序。先复制 package.json(少变)再复制源码(每次提交变)。每个变化层使其下所有层失效。
CMD 和 ENTRYPOINT 有什么区别?
ENTRYPOINT 是固定命令(如 node server.js);CMD 是可覆盖默认参数(docker run <img> --port 4000)。容器为单一可执行文件用 ENTRYPOINT,通用基础用 CMD。