Skip to content

Docker 镜像构建最佳实践

本章将深入讲解 Docker 镜像构建的最佳实践,包括 Dockerfile 优化、构建性能提升、安全配置等关键技巧。

镜像构建原则

核心设计原则

  1. 最小化原则:只包含必要的组件和文件
  2. 单一职责:每个容器只运行一个主要服务
  3. 不可变性:镜像构建后不应修改
  4. 可重现性:相同的构建环境产生相同的镜像
  5. 安全性:遵循安全最佳实践

镜像分层策略

dockerfile
# 良好的分层示例
FROM node:16-alpine AS base
# 基础层:很少变化

WORKDIR /app
# 工作目录层:很少变化

COPY package*.json ./
# 依赖文件层:偶尔变化

RUN npm ci --only=production
# 依赖安装层:偶尔变化

COPY . .
# 应用代码层:经常变化

RUN npm run build
# 构建层:经常变化

Dockerfile 优化技巧

基础镜像选择

dockerfile
# ❌ 避免使用过大的基础镜像
FROM ubuntu:20.04

# ✅ 优先使用 Alpine 版本
FROM node:16-alpine

# ✅ 使用 distroless 镜像(生产环境)
FROM gcr.io/distroless/nodejs:16

# ✅ 使用 scratch(静态编译程序)
FROM scratch
COPY myapp /
ENTRYPOINT ["/myapp"]

指令优化

dockerfile
# ❌ 不好的做法 - 多个 RUN 指令
FROM alpine:3.16
RUN apk update
RUN apk add curl
RUN apk add vim
RUN rm -rf /var/cache/apk/*

# ✅ 好的做法 - 合并 RUN 指令
FROM alpine:3.16
RUN apk update && \
    apk add --no-cache curl vim && \
    rm -rf /var/cache/apk/*

# ✅ 更好的做法 - 使用 heredoc
FROM alpine:3.16
RUN <<EOF
apk update
apk add --no-cache curl vim
rm -rf /var/cache/apk/*
EOF

缓存优化

dockerfile
# ✅ 优化构建缓存的 Dockerfile
FROM node:16-alpine

WORKDIR /app

# 1. 先复制依赖文件(变化频率低)
COPY package*.json ./

# 2. 安装依赖(利用缓存)
RUN npm ci --only=production && npm cache clean --force

# 3. 后复制源代码(变化频率高)
COPY . .

# 4. 构建应用
RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

文件复制优化

dockerfile
# ❌ 复制不必要的文件
COPY . /app

# ✅ 使用 .dockerignore 排除文件
# .dockerignore 内容:
# node_modules
# .git
# *.log
# .env

# ✅ 精确复制需要的文件
COPY package*.json ./
COPY src/ ./src/
COPY public/ ./public/

# ✅ 使用通配符
COPY *.json ./
COPY src/ ./src/

多阶段构建

基本多阶段构建

dockerfile
# 构建阶段
FROM node:16 AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# 生产阶段
FROM node:16-alpine AS production

WORKDIR /app

# 只复制必要文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

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

USER nextjs

EXPOSE 3000
CMD ["npm", "start"]

复杂多阶段构建

dockerfile
# 依赖阶段
FROM node:16-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# 构建阶段
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 测试阶段
FROM builder AS tester
RUN npm run test
RUN npm run lint

# 生产阶段
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# 复制必要文件
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json

USER nextjs

EXPOSE 3000

CMD ["npm", "start"]

构建特定阶段

bash
# 只构建到测试阶段
docker build --target tester -t myapp:test .

# 构建生产镜像
docker build --target runner -t myapp:prod .

# 并行构建多个阶段
docker build --target deps -t myapp:deps . &
docker build --target builder -t myapp:builder . &
wait

安全最佳实践

用户和权限

dockerfile
# ✅ 创建和使用非 root 用户
FROM alpine:3.16

# 创建用户
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# 设置工作目录权限
WORKDIR /app
RUN chown appuser:appgroup /app

# 复制文件并设置权限
COPY --chown=appuser:appgroup . .

# 切换到非 root 用户
USER appuser

# 设置只读根文件系统
# 运行时使用: docker run --read-only myapp

最小权限原则

dockerfile
FROM alpine:3.16

# 只安装必要的包
RUN apk add --no-cache \
    ca-certificates \
    && rm -rf /var/cache/apk/*

# 删除不必要的工具
RUN apk del --purge \
    wget \
    curl

# 设置安全的文件权限
COPY --chmod=755 app /usr/local/bin/app
COPY --chmod=644 config.json /etc/app/config.json

USER 1001:1001

ENTRYPOINT ["/usr/local/bin/app"]

秘密信息处理

dockerfile
# ❌ 不要在镜像中硬编码秘密
ENV API_KEY=secret123
COPY secret.key /app/

# ✅ 使用构建时参数
ARG API_URL
ENV API_URL=${API_URL}

# ✅ 使用多阶段构建处理秘密
FROM alpine AS secrets
ARG SECRET_KEY
RUN echo "$SECRET_KEY" > /tmp/secret

FROM alpine AS final
RUN --mount=from=secrets,source=/tmp/secret,target=/secret \
    process_secret < /secret

性能优化

镜像大小优化

dockerfile
# Python 应用优化示例
FROM python:3.9-slim AS builder

# 安装构建依赖
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        gcc \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 生产阶段
FROM python:3.9-slim

# 只复制必要的 Python 包
COPY --from=builder /root/.local /root/.local

# 确保脚本在 PATH 中
ENV PATH=/root/.local/bin:$PATH

WORKDIR /app
COPY . .

# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash app && \
    chown -R app:app /app
USER app

CMD ["python", "app.py"]

构建缓存优化

dockerfile
# Go 应用构建优化
FROM golang:1.19-alpine AS builder

WORKDIR /app

# 利用 Go modules 缓存
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# 最小运行时镜像
FROM scratch

# 复制 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 复制二进制文件
COPY --from=builder /app/main /main

EXPOSE 8080
ENTRYPOINT ["/main"]

BuildKit 优化

dockerfile
# syntax=docker/dockerfile:1

FROM node:16-alpine

WORKDIR /app

# 使用缓存挂载
RUN --mount=type=cache,target=/root/.npm \
    npm install -g npm@latest

# 使用绑定挂载
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=package-lock.json,target=package-lock.json \
    --mount=type=cache,target=/root/.npm \
    npm ci --only=production

COPY . .

CMD ["npm", "start"]

特定语言优化

Node.js 应用

dockerfile
FROM node:16-alpine AS base

# 安装 dumb-init
RUN apk add --no-cache dumb-init

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

FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

FROM base AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM base AS runtime
WORKDIR /app

ENV NODE_ENV production

COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nextjs:nodejs /app/dist ./dist
COPY --from=build --chown=nextjs:nodejs /app/package.json ./package.json

USER nextjs

EXPOSE 3000

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

Python 应用

dockerfile
FROM python:3.9-slim AS base

# 设置 Python 环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

FROM base AS builder

# 安装构建依赖
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM base AS runtime

# 复制 Python 包
COPY --from=builder /root/.local /root/.local

# 确保用户安装的包在 PATH 中
ENV PATH=/root/.local/bin:$PATH

# 创建用户
RUN useradd --create-home --shell /bin/bash app

WORKDIR /app
COPY --chown=app:app . .

USER app

EXPOSE 8000

CMD ["python", "app.py"]

Java 应用

dockerfile
FROM openjdk:11-jdk-slim AS builder

WORKDIR /app
COPY pom.xml .
COPY src ./src

# 构建应用
RUN ./mvnw clean package -DskipTests

FROM openjdk:11-jre-slim AS runtime

# 创建用户
RUN useradd --create-home --shell /bin/bash app

WORKDIR /app

# 复制 JAR 文件
COPY --from=builder --chown=app:app /app/target/*.jar app.jar

USER app

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

构建工具和技巧

使用 BuildKit

bash
# 启用 BuildKit
export DOCKER_BUILDKIT=1

# 或在构建时启用
DOCKER_BUILDKIT=1 docker build -t myapp .

# 使用 buildx
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp .

构建参数和变量

dockerfile
# 定义构建参数
ARG NODE_VERSION=16
ARG BUILD_DATE
ARG VERSION

FROM node:${NODE_VERSION}-alpine

# 设置标签
LABEL build-date=${BUILD_DATE}
LABEL version=${VERSION}

# 使用构建参数
ARG API_URL=https://api.example.com
ENV API_URL=${API_URL}

# 条件构建
ARG ENVIRONMENT=production
RUN if [ "$ENVIRONMENT" = "development" ]; then \
        npm install; \
    else \
        npm ci --only=production; \
    fi
bash
# 传递构建参数
docker build \
  --build-arg NODE_VERSION=18 \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  --build-arg VERSION=1.0.0 \
  -t myapp:1.0.0 .

构建脚本

bash
#!/bin/bash
# build.sh

set -e

# 配置变量
IMAGE_NAME="myapp"
VERSION=${1:-latest}
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT=$(git rev-parse --short HEAD)

echo "构建镜像: $IMAGE_NAME:$VERSION"

# 构建镜像
docker build \
  --build-arg VERSION=$VERSION \
  --build-arg BUILD_DATE=$BUILD_DATE \
  --build-arg GIT_COMMIT=$GIT_COMMIT \
  --tag $IMAGE_NAME:$VERSION \
  --tag $IMAGE_NAME:latest \
  .

# 运行测试
echo "运行测试..."
docker run --rm $IMAGE_NAME:$VERSION npm test

# 安全扫描
echo "执行安全扫描..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $IMAGE_NAME:$VERSION

echo "构建完成: $IMAGE_NAME:$VERSION"

镜像标签策略

语义化版本

bash
# 版本标签策略
docker build -t myapp:1.2.3 .          # 具体版本
docker build -t myapp:1.2 .            # 次版本
docker build -t myapp:1 .              # 主版本
docker build -t myapp:latest .         # 最新版本

# Git 标签策略
docker build -t myapp:$(git describe --tags) .
docker build -t myapp:$(git rev-parse --short HEAD) .

环境标签

bash
# 环境相关标签
docker build -t myapp:dev .
docker build -t myapp:staging .
docker build -t myapp:prod .

# 带时间戳的标签
docker build -t myapp:$(date +%Y%m%d-%H%M%S) .

质量检查

Dockerfile Linting

bash
# 使用 hadolint 检查 Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile

# 或安装本地版本
brew install hadolint
hadolint Dockerfile

镜像扫描

bash
# 使用 Trivy 扫描漏洞
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image myapp:latest

# 使用 Clair 扫描
docker run -d --name clair-db postgres:latest
docker run -d --name clair --link clair-db:postgres \
  quay.io/coreos/clair:latest

# 使用 Docker Bench Security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh

自动化测试

yaml
# .github/workflows/docker.yml
name: Docker Build and Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Build Docker image
      run: docker build -t myapp:test .
    
    - name: Run tests
      run: docker run --rm myapp:test npm test
    
    - name: Security scan
      run: |
        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
          aquasec/trivy image myapp:test
    
    - name: Push to registry
      if: github.ref == 'refs/heads/main'
      run: |
        echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
        docker tag myapp:test myapp:latest
        docker push myapp:latest

本章小结

本章详细介绍了 Docker 镜像构建的最佳实践:

关键要点:

  • 优化原则:最小化、单一职责、安全性
  • Dockerfile 优化:合并指令、缓存利用、文件复制
  • 多阶段构建:分离构建和运行环境
  • 安全实践:非 root 用户、最小权限、秘密管理
  • 性能优化:镜像大小、构建速度、缓存策略
  • 质量保证:代码检查、安全扫描、自动化测试

最佳实践总结:

  • 选择合适的基础镜像
  • 优化 Dockerfile 指令顺序
  • 使用多阶段构建
  • 实施安全配置
  • 建立质量检查流程
  • 自动化构建和测试

掌握这些最佳实践将帮助你构建更小、更安全、更高效的 Docker 镜像。

延伸阅读

本站内容仅供学习和研究使用。