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的迁移路径已经很成熟,团队不需要重复学习容器编排的基础概念。