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 包(如 sharp、bcrypt)需要额外配置。如果遇到兼容性问题,先用 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条评论