1. 从手动部署到容器编排:一个真实项目的痛点

上个月接手一个电商后台项目,服务端由4个模块组成:Nginx网关、Spring Boot主服务、PostgreSQL数据库和Redis缓存。最初团队采用shell脚本逐个启动,每次发布都要经历以下流程:检查依赖服务是否就绪→启动数据库→等待端口响应→启动Redis→最后启动Spring Boot。整个过程耗时约5分钟,而且经常出现Spring Boot启动时数据库还没准备好的情况,导致连接池报错。

更头疼的是环境一致性问题。开发环境、测试环境、生产环境的配置文件各不相同,每次部署都要手动修改。后来决定全面转向Docker Compose,目标很简单:一条命令启动所有服务,依赖顺序自动控制,环境配置统一管理。

2. 环境与版本选择

本次使用的核心版本如下:
- Docker Engine:24.0.7(支持Compose V2)
- Docker Compose:v2.21.0
- 基础镜像:postgres:16.1-alpine、redis:7.2.3-alpine、openjdk:17-slim、nginx:1.25.3-alpine

选alpine版本是因为镜像体积平均比标准版小50%以上。PostgreSQL和Redis采用官方镜像,Spring Boot应用使用多阶段构建生成精简镜像,最终镜像大小控制在180MB左右。

3. 网络与存储方案设计

网络方面选择bridge驱动自定义网络,而不是默认的default网络。原因有三:自定义网络自带DNS解析,服务名可以直接作为主机名通信;隔离性好,不同项目之间互不干扰;便于后续扩展。

卷挂载设计了三种类型:
- bind mount:用于Nginx配置文件和Spring Boot的日志目录,方便实时修改和查看
- named volume:用于PostgreSQL数据持久化,保证容器重建后数据不丢失
- tmpfs:用于Redis持久化,兼顾性能和数据安全

4. 核心实现:完整的docker-compose.yml配置

以下是经过测试可运行的完整配置:

version: "3.8"

services:
  postgres:
    image: postgres:16.1-alpine
    container_name: ecommerce-postgres
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d ecommerce"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - backend

  redis:
    image: redis:7.2.3-alpine
    container_name: ecommerce-redis
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    tmpfs:
      - /var/log/redis
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - backend

  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: ecommerce-app
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ecommerce
      SPRING_DATASOURCE_USERNAME: admin
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      SPRING_REDIS_HOST: redis
      SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
    volumes:
      - ./logs:/app/logs
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    networks:
      - backend
      - frontend

  nginx:
    image: nginx:1.25.3-alpine
    container_name: ecommerce-nginx
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./ssl:/etc/nginx/ssl:ro
    ports:
      - "80:80"
      - "443:443"
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - frontend

volumes:
  pgdata:
  redisdata:

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

5. 踩坑记录与优化方案

坑1:depends_on的默认行为
最开始以为depends_on能保证依赖服务完全就绪,后来发现它只控制启动顺序,不会等待服务真正可用。Spring Boot启动时数据库还没初始化完成,照样报连接错误。解决方案就是上面的healthcheck + condition: service_healthy组合,实测能100%解决依赖问题。

坑2:PostgreSQL健康检查超时
pg_isready命令在容器启动初期可能返回"accepting connections"但实际数据库还在初始化。需要设置start_period参数,给数据库留出足够的初始化时间。我设置为30秒,实际生产环境建议60秒以上。

坑3:Redis密码导致healthcheck失败
Redis启用密码后,redis-cli ping需要认证。在healthcheck中必须显式传入密码,否则检查永远失败。

坑4:数据卷权限问题
PostgreSQL容器以postgres用户运行,如果宿主机的数据卷目录权限不对,会导致启动失败。解决方法是提前创建目录并设置属主:

mkdir -p /data/pgdata && chown 999:999 /data/pgdata

优化1:日志轮转
Spring Boot应用日志如果不加限制,一天能产生几个GB。配置logrotate或者使用JSON-file日志驱动限制大小:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "5"

优化2:镜像体积控制
Spring Boot应用使用分层构建,基础镜像选择eclipse-temurin:17-jre-alpine,排除不需要的依赖。最终镜像从400MB降到180MB,部署时间明显缩短。

6. 部署效果与性能数据

改造完成后,部署流程从原来的5分钟缩短到45秒(包括镜像拉取时间)。资源占用方面,容器化部署比虚拟机部署节省约30%内存,主要得益于轻量级镜像和资源共享机制。

线上运行两周的数据:
- 服务可用性:99.95%(之前为99.2%)
- 平均启动时间:45秒(之前为5分钟)
- 容器重启次数:0次(之前手动部署出现过3次故障)
- 磁盘占用:日志从每天2GB降到500MB左右

7. 总结与建议

Docker Compose解决的不只是部署流程问题,更重要的是让团队有了统一的环境管理标准。现在新同事加入只需要安装Docker和Compose,运行docker-compose up -d就能开始开发,环境配置问题基本消失。

几点经验供参考:
1. 健康检查一定要做细,不要只检查端口,要检查业务层面的就绪状态
2. 生产环境建议固定镜像版本,不要用latest标签
3. 敏感信息通过环境变量注入,不要写死在配置文件中
4. 数据卷和bind mount要区分使用场景,数据库必须用named volume

后续计划接入Kubernetes,把Compose配置转换为Helm charts。从Compose到K8s的迁移路径已经很成熟,团队不需要重复学习容器编排的基础概念。