Docker已经成为现代软件开发和部署的标准工具。然而,仅仅会用Docker是远远不够的,掌握最佳实践才能构建出安全、高效、可维护的容器化应用。本文将分享从开发到生产的完整Docker最佳实践。

Docker镜像优化

1. 选择合适的基础镜像

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ❌ 使用最新的Ubuntu镜像(体积大、攻击面大)
FROM ubuntu:latest

# ✅ 使用官方轻量级镜像
FROM alpine:3.18

# ✅ 使用特定版本的官方镜像
FROM node:18-alpine3.18

# ✅ 使用distroless镜像(最小攻击面)
FROM gcr.io/distroless/nodejs18-debian11

2. 多阶段构建优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app

# 只复制依赖文件,利用Docker缓存
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# 复制源代码并构建
COPY . .
RUN npm run build

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

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

WORKDIR /app

# 从构建阶段复制产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

# 切换到非root用户
USER nodejs

EXPOSE 3000
CMD ["node", "dist/index.js"]

3. 镜像层优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ❌ 每次RUN都创建新层
FROM node:18-alpine
RUN apt-get update
RUN apt-get install -y git
RUN apt-get install -y curl
RUN apt-get install -y wget

# ✅ 合并命令,减少层数
FROM node:18-alpine
RUN apt-get update && \
    apt-get install -y git curl wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# ✅ 使用.dockerignore排除不必要的文件
# .dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
.nyc_output
coverage
.coverage
.nyc_output
.vscode

4. 镜像安全扫描

1
2
3
4
5
6
7
8
9
# 使用Trivy扫描镜像漏洞
trivy image myapp:latest

# 使用Docker Scout(新版本Docker内置)
docker scout cves myapp:latest

# 在CI/CD中集成安全扫描
docker build -t myapp:latest .
docker scout cves myapp:latest --exit-code

容器安全最佳实践

1. 用户权限管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ❌ 以root用户运行容器
FROM node:18-alpine

# ✅ 创建并使用非root用户
FROM node:18-alpine

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

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

USER appuser

2. 网络安全配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    networks:
      - frontend
      - backend
    # 只暴露必要的端口
    expose:
      - "3000"

  database:
    image: postgres:15
    networks:
      - backend
    # 不暴露到外部网络
    expose: []

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true # 内部网络,不能访问外网

3. 资源限制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 设置资源限制
services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    ulimits:
      nproc: 65535
      nofile:
        soft: 20000
        hard: 40000

环境配置管理

1. 多环境配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Dockerfile
FROM node:18-alpine

ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV

# 根据环境安装不同的依赖
COPY package*.json ./
RUN if [ "$NODE_ENV" = "production" ]; then \
        npm ci --only=production; \
    else \
        npm ci; \
    fi

# 多阶段构建支持不同环境
FROM node:18-alpine AS development
RUN npm ci
COPY . .

FROM node:18-alpine AS production
RUN npm ci --only=production
COPY --from=development /app/dist ./dist

2. 配置文件管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用环境变量配置
version: '3.8'

services:
  web:
    image: myapp:latest
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}
    env_file:
      - ./config/production.env
      - ./config/secrets.env
1
2
3
4
5
6
7
8
# production.env
NODE_ENV=production
PORT=3000
LOG_LEVEL=info

# secrets.env(不提交到版本控制)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-secret-key

3. 配置注入最佳实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用配置文件挂载
docker run -d \
  --name myapp \
  -v $(pwd)/config.json:/app/config.json:ro \
  myapp:latest

# 使用环境变量文件
docker run -d \
  --name myapp \
  --env-file ./production.env \
  myapp:latest

# 使用Docker secrets(Swarm模式)
echo "my-secret-password" | docker secret create db_password -

数据持久化策略

1. 数据卷管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: '3.8'

services:
  database:
    image: postgres:15
    volumes:
      # 使用命名卷
      - postgres_data:/var/lib/postgresql/data
      # 使用绑定挂载(仅开发环境)
      - ./init-scripts:/docker-entrypoint-initdb.d:ro

  app:
    image: myapp:latest
    volumes:
      - app_data:/app/data
      - app_logs:/app/logs
      # 临时文件使用tmpfs
      - type: tmpfs
        target: /app/tmp

volumes:
  postgres_data:
    driver: local
  app_data:
    driver: local
  app_logs:
    driver: local

2. 备份和恢复策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash
# backup.sh - 数据库备份脚本

BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
CONTAINER_NAME="postgres"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 备份数据库
docker exec $CONTAINER_NAME pg_dump -U postgres myapp > "$BACKUP_DIR/backup_$DATE.sql"

# 保留最近30天的备份
find $BACKUP_DIR -name "backup_*.sql" -mtime +30 -delete

echo "Backup completed: backup_$DATE.sql"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# restore.sh - 数据库恢复脚本

if [ $# -eq 0 ]; then
    echo "Usage: $0 <backup_file>"
    exit 1
fi

BACKUP_FILE=$1
CONTAINER_NAME="postgres"

# 停止应用连接
docker stop app

# 恢复数据库
docker exec -i $CONTAINER_NAME psql -U postgres -d myapp < $BACKUP_FILE

# 重启应用
docker start app

echo "Database restored from $BACKUP_FILE"

监控和日志管理

1. 应用监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
version: '3.8'

services:
  app:
    image: myapp:latest
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
    environment:
      - METRICS_ENABLED=true

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro

  node-exporter:
    image: prom/node-exporter:latest
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro

volumes:
  prometheus_data:
  grafana_data:

2. 日志管理策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 应用日志配置
FROM node:18-alpine

# 创建日志目录
RUN mkdir -p /app/logs && \
    chown -R nodejs:nodejs /app/logs

# 使用健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# 设置日志驱动
# 在docker-compose中配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# docker-compose.yml
version: '3.8'

services:
  app:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        labels: "app,production"
    labels:
      - "monitoring.logging=true"

  # 使用ELK Stack进行日志聚合
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"

  logstash:
    image: docker.elastic.co/logstash/logstash:8.5.0
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline:ro

  kibana:
    image: docker.elastic.co/kibana/kibana:8.5.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200

CI/CD集成

1. GitHub Actions工作流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# .github/workflows/docker.yml
name: Docker Build and Deploy

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: myorg/myapp
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

2. 安全扫描集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 添加到CI工作流中
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t myapp:test .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:test'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

生产环境部署

1. 多阶段部署策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# docker-compose.prod.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - app

  app:
    image: myorg/myapp:${VERSION}
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    volumes:
      - app_data:/app/data
      - app_logs:/app/logs
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    restart: unless-stopped

  database:
    image: postgres:15
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    restart: unless-stopped

volumes:
  nginx_logs:
  app_data:
  app_logs:
  redis_data:
  postgres_data:

2. 零停机部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/bin/bash
# deploy.sh - 零停机部署脚本

set -e

APP_NAME="myapp"
NEW_VERSION=$1
CURRENT_CONTAINER="${APP_NAME}_current"
NEW_CONTAINER="${APP_NAME}_new"

if [ -z "$NEW_VERSION" ]; then
    echo "Usage: $0 <version>"
    exit 1
fi

echo "Deploying version $NEW_VERSION"

# 拉取新镜像
docker pull myorg/$APP_NAME:$NEW_VERSION

# 启动新容器
docker run -d \
    --name $NEW_CONTAINER \
    --env-file ./production.env \
    -e NODE_ENV=production \
    --network app-network \
    myorg/$APP_NAME:$NEW_VERSION

# 健康检查
echo "Waiting for new container to be healthy..."
for i in {1..30}; do
    if docker exec $NEW_CONTAINER curl -f http://localhost:3000/health; then
        echo "New container is healthy"
        break
    fi
    if [ $i -eq 30 ]; then
        echo "Health check failed"
        docker logs $NEW_CONTAINER
        docker stop $NEW_CONTAINER
        docker rm $NEW_CONTAINER
        exit 1
    fi
    sleep 2
done

# 切换流量
docker stop $CURRENT_CONTAINER
docker rename $CURRENT_CONTAINER "${APP_NAME}_old"
docker rename $NEW_CONTAINER $CURRENT_CONTAINER

# 清理旧容器(延迟删除,便于回滚)
sleep 30
docker rm "${APP_NAME}_old" 2>/dev/null || true

echo "Deployment completed successfully"

3. 回滚策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/bin/bash
# rollback.sh - 回滚脚本

set -e

APP_NAME="myapp"
TARGET_VERSION=$1
CURRENT_CONTAINER="${APP_NAME}_current"

if [ -z "$TARGET_VERSION" ]; then
    echo "Usage: $0 <target_version>"
    echo "Available versions:"
    docker images myorg/$APP_NAME --format "table {{.Tag}}"
    exit 1
fi

echo "Rolling back to version $TARGET_VERSION"

# 创建回滚容器
ROLLBACK_CONTAINER="${APP_NAME}_rollback"
docker run -d \
    --name $ROLLBACK_CONTAINER \
    --env-file ./production.env \
    -e NODE_ENV=production \
    --network app-network \
    myorg/$APP_NAME:$TARGET_VERSION

# 健康检查
echo "Waiting for rollback container to be healthy..."
for i in {1..30}; do
    if docker exec $ROLLBACK_CONTAINER curl -f http://localhost:3000/health; then
        echo "Rollback container is healthy"
        break
    fi
    if [ $i -eq 30 ]; then
        echo "Rollback health check failed"
        docker stop $ROLLBACK_CONTAINER
        docker rm $ROLLBACK_CONTAINER
        exit 1
    fi
    sleep 2
done

# 执行回滚
docker stop $CURRENT_CONTAINER
docker rename $CURRENT_CONTAINER "${APP_NAME}_failed"
docker rename $ROLLBACK_CONTAINER $CURRENT_CONTAINER

# 清理失败容器
docker rm "${APP_NAME}_failed" 2>/dev/null || true

echo "Rollback to $TARGET_VERSION completed successfully"

性能优化技巧

1. 镜像大小优化

1
2
3
4
5
6
7
8
# 分析镜像层大小
docker history myapp:latest

# 使用dive工具深入分析
dive myapp:latest

# 查找大文件
docker run --rm -it myapp:latest du -ah / | sort -rh | head -n 20

2. 容器启动优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 减少启动时间
FROM node:18-alpine

# 预安装常用包
RUN apk add --no-cache dumb-init

# 使用dumb-init作为PID 1
ENTRYPOINT ["dumb-init", "--"]

# 预热Node.js
ENV NODE_OPTIONS="--max-old-space-size=512"

# 优化应用启动
CMD ["node", "--max-old-space-size=512", "dist/index.js"]

3. 网络优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 使用自定义网络
version: '3.8'

services:
  app:
    image: myapp:latest
    networks:
      - app-network
    # 优化DNS设置
    dns:
      - 8.8.8.8
      - 8.8.4.4

networks:
  app-network:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: app-br
      com.docker.network.driver.mtu: 1500

故障排除指南

1. 常见问题诊断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 查看容器日志
docker logs myapp
docker logs myapp --tail 100  # 最后100行
docker logs myapp --follow     # 实时日志

# 进入容器调试
docker exec -it myapp /bin/sh
docker run -it --rm myapp:latest /bin/sh  # 临时容器

# 检查容器资源使用
docker stats myapp
docker top myapp

# 检查网络连接
docker exec myapp netstat -tulpn
docker network inspect app-network

2. 性能分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 分析容器启动时间
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  nathanleclaire/docker-image-size-cmp myapp:latest

# 使用cAdvisor监控
docker run -d \
  --name=cadvisor \
  -p 8080:8080 \
  -v /:/rootfs:ro \
  -v /var/run:/var/run:ro \
  -v /sys:/sys:ro \
  -v /var/lib/docker/:/var/lib/docker:ro \
  gcr.io/cadvisor/cadvisor:latest

总结

Docker最佳实践涵盖了从镜像构建到生产部署的方方面面:

核心要点

  1. 镜像优化:多阶段构建、减少层数、使用轻量级基础镜像
  2. 安全加固:非root用户、网络隔离、资源限制
  3. 环境管理:环境变量、配置文件、多环境支持
  4. 数据管理:持久化策略、备份恢复
  5. 监控日志:应用监控、日志聚合、性能分析
  6. CI/CD集成:自动化构建、安全扫描、部署策略

生产就绪检查清单

  • 镜像安全扫描通过
  • 使用非root用户运行
  • 资源限制已配置
  • 健康检查已设置
  • 日志管理策略已实现
  • 备份恢复策略已制定
  • 监控告警已配置
  • 回滚策略已测试

通过遵循这些最佳实践,你可以构建出安全、高效、可维护的Docker容器化应用,为现代化软件开发奠定坚实的基础。记住,容器化是一个持续优化的过程,需要根据实际需求和反馈不断调整和改进。