WSの小屋

Docker 容器化最佳实践:镜像优化与多阶段构建

容器化已成为现代应用部署的标配,但很多人的 Docker 镜像存在体积过大、构建缓慢、安全隐患等问题。本文从实际生产项目出发,系统讲解 Docker 优化的关键技巧。

镜像体积优化

多阶段构建:最有效的优化手段

未优化的镜像通常包含构建工具和源码,生产环境只需要最终产物。多阶段构建可以让最终镜像只包含运行时所需文件。

# ===== 第一阶段:构建 =====
FROM node:20-alpine AS builder
WORKDIR /app

# 先拷贝依赖文件,利用 Docker 缓存层
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

# 拷贝源码并构建
COPY . .
RUN pnpm build && pnpm prune --prod

# ===== 第二阶段:运行 =====
FROM node:20-alpine AS runner
WORKDIR /app

# 安装 curl 用于健康检查
RUN apk add --no-cache curl

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# 只拷贝必要文件
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

优化效果对比

阶段 基础镜像 大小 说明
优化前 node:20 ~1.2GB 包含完整构建工具链
第一阶段 node:20-alpine ~350MB Alpine + 构建产物
最终镜像 node:20-alpine ~180MB 只有运行时文件

选择合适的基础镜像

# 完整版:~1GB,包含所有工具(不推荐生产使用)
FROM node:20

# Alpine 版:~150MB,最小体积(推荐)
FROM node:20-alpine

# slim 版:~200MB,Debian 精简版(兼容性更好)
FROM node:20-slim

# distroless:~120MB,无 shell,最安全
FROM gcr.io/distroless/nodejs20-debian12

Alpine 的坑:Alpine 使用 musl libc 而非 glibc,某些 npm 包(如 sharpbcrypt)需要额外配置。如果遇到兼容性问题,先用 slim 替代。

.dockerignore:减少构建上下文

# .dockerignore
node_modules
dist
.git
.gitignore
.env*
.vscode
coverage
*.md
Dockerfile
docker-compose*.yml

没有 .dockerignore 时,COPY . . 会把 node_modules(可能有几百 MB)也拷贝到构建上下文中。

构建速度优化

利用缓存层

Docker 每条指令生成一层,只有输入变化时才重新执行。把变化频率低的放前面:

# ✅ 正确顺序:依赖文件先于源码
COPY package.json pnpm-lock.yaml ./    # 变化少,缓存命中率高
RUN pnpm install --frozen-lockfile     # 依赖不变则跳过
COPY . .                               # 源码经常变
RUN pnpm build                         # 每次都执行

# ❌ 错误顺序:一次 COPY 导致缓存全部失效
COPY . .
RUN pnpm install && pnpm build

BuildKit 并行构建

# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# 并行执行(--mount=type=cache 共享缓存)
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/app/.pnpm-store \
    pnpm build

FROM node:20-alpine AS tester
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm test

# 最终阶段
FROM node:20-alpine
COPY --from=builder /app/dist /app/dist
COPY --from=deps /app/node_modules /app/node_modules
CMD ["node", "/app/dist/server.js"]

启用 BuildKit:

# 命令行启用
DOCKER_BUILDKIT=1 docker build -t myapp .

# 或 docker compose
COMPOSE_DOCKER_CLI_BUILD=1 docker compose build

pnpm 的 --frozen-lockfile

# --frozen-lockfile 确保 lockfile 不被修改,CI 更安全
RUN pnpm install --frozen-lockfile --prod

安全加固

非 root 用户运行

# 创建专用用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

只读文件系统

# docker-compose.yml
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
      - /app/logs

使用 distroless 镜像

# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# 运行阶段:distroless 无 shell,无法被攻击者 exec
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["dist/server.js"]

扫描镜像漏洞

# 使用 Trivy 扫描
trivy image myapp:latest

# 使用 Docker Scout
docker scout cves myapp:latest

Dockerfile 最佳实践检查清单

# 1. 指定固定版本(避免 :latest)
FROM node:20.11-alpine3.19

# 2. 合并 RUN 指令减少层数
RUN apk add --no-cache curl git && \
    rm -rf /var/cache/apk/*

# 3. 清理安装缓存
RUN pnpm install --frozen-lockfile && \
    pnpm store prune

# 4. 使用 LABEL 标注元信息
LABEL maintainer="dev@team.com"
LABEL version="1.0.0"
LABEL description="Production image for My App"

# 5. 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000

生产环境 Docker Compose

# docker-compose.prod.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:

多架构构建

# 创建 buildx builder
docker buildx create --name mybuilder --use

# 构建 amd64 + arm64 镜像
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:latest \
  --push .
# Dockerfile 自动适配架构
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
ARG TARGETPLATFORM
RUN echo "Building on $BUILDPLATFORM, targeting $TARGETPLATFORM"

CI/CD 集成

# .github/workflows/deploy.yml
name: Build and Deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

总结

Docker 镜像优化的优先级:

优化项 体积减少 难度 优先级
多阶段构建 60-80% ⭐⭐⭐
Alpine 基础镜像 70% ⭐⭐⭐
.dockerignore 减少上下文 ⭐⭐⭐
缓存层优化 加快构建 ⭐⭐
非 root 用户 安全提升 ⭐⭐
distroless 镜像 安全+体积
多架构构建 跨平台

建议从多阶段构建 + Alpine + .dockerignore 三件套开始,这三步就能把镜像从 1.2GB 降到 200MB 以内。

Comments | 0条评论