一、问题背景:手工docker run管理多服务的痛
上个月接手一个老项目,6个服务靠一堆shell脚本启动,每次部署都要手动敲docker run,端口冲突、依赖顺序错乱、数据丢失是家常便饭。尤其有一次Redis先于PostgreSQL启动,应用连不上库直接崩溃,排查了半天才发现是启动顺序问题。
后来决定用Docker Compose重构,但网上教程大多只讲单机单服务,真正生产级的多服务编排,网络、卷、健康检查这些关键点都语焉不详。这篇文章就分享下我实际落地的配置,踩过的坑和优化后的最终方案。
二、环境与版本
Docker Engine: 24.0.7
Docker Compose: v2.24.2
操作系统: Ubuntu 22.04 LTS
服务器: 4核8G 阿里云ECS
项目组成:
- nginx:前端静态资源 + 反向代理,版本1.25.3
- backend:Spring Boot 3.2.0,打包成jar
- postgres:业务数据库,版本16.1
- redis:缓存,版本7.2.3
三、方案设计:网络隔离 + 依赖控制
先说设计思路。Compose文件我拆成两部分:docker-compose.yml(基础配置)和docker-compose.prod.yml(生产覆盖)。核心要解决三个问题:
1. 网络隔离:所有服务挂在同一个自定义bridge网络app_net下,服务间通过服务名通信(Compose内置DNS解析),外部只能通过Nginx暴露80端口。
2. 数据持久化:PostgreSQL和Redis用命名卷(named volume)存储数据,避免容器删除后数据丢失。注意不是bind mount,bind mount依赖宿主机目录结构,可移植性差。
3. 启动顺序与健康检查:这是最关键的部分。Compose的depends_on在2.x版本默认只控制启动顺序,不等待服务就绪。必须配合healthcheck才能实现“等数据库真正可用了再启动后端”。
四、核心实现:完整docker-compose配置
直接上配置,这是我在生产环境跑的版本,去掉了敏感信息:
# docker-compose.yml
version: "3.8"
services:
nginx:
image: nginx:1.25.3-alpine
container_name: nginx-gateway
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./dist:/usr/share/nginx/html:ro
networks:
- app_net
depends_on:
backend:
condition: service_healthy
restart: always
backend:
build: ./backend
image: myapp/backend:1.0.0
container_name: backend-service
expose:
- "8080" # 仅内部网络可访问
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_HOST=postgres
- REDIS_HOST=redis
volumes:
- ./logs:/app/logs
networks:
- app_net
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
restart: always
postgres:
image: postgres:16.1-alpine
container_name: postgres-db
environment:
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=myapp_db
volumes:
- pg_data:/var/lib/postgresql/data
- ./backup:/backup
networks:
- app_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp -d myapp_db"]
interval: 5s
timeout: 3s
retries: 10
restart: always
redis:
image: redis:7.2.3-alpine
container_name: redis-cache
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
networks:
- app_net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: always
networks:
app_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
volumes:
pg_data:
driver: local
redis_data:
driver: local
这里有几个细节必须说明:
exposevsports:backend只对内部网络暴露8080,不映射到宿主机。Nginx通过http://backend:8080反向代理,安全且减少端口占用。- 健康检查命令:PostgreSQL用
pg_isready,Redis用redis-cli ping,Spring Boot用curl打actuator端点。注意Spring Boot镜像里可能没有curl,我是在Dockerfile里装的。 start_period参数:给JVM启动留了40秒缓冲,避免启动慢导致误判为不健康。
生产环境覆盖文件:
# docker-compose.prod.yml
version: "3.8"
services:
backend:
environment:
- JAVA_OPTS=-Xmx2g -Xms2g
- DB_PASSWORD=${DB_PASSWORD}
deploy:
resources:
limits:
cpus: "2.0"
memory: 3g
reservations:
cpus: "0.5"
memory: 1g
postgres:
deploy:
resources:
limits:
memory: 2g
五、踩坑与优化:三个真实教训
坑1:depends_on不等待就绪
最开始我写的是:
depends_on:
- postgres
- redis
结果后端容器在PostgreSQL还没初始化完就启动,连不上库直接退出。后来改成condition: service_healthy才解决。注意只有Compose 2.x才支持这个语法,老版本1.x只能用wait-for-it.sh脚本。
坑2:健康检查命令不存在
Spring Boot的健康检查我一开始用的是wget -qO- http://localhost:8080/actuator/health,结果容器一直报unhealthy。查了半天发现基础镜像eclipse-temurin:17-jre-alpine里没有wget。换成curl后需要额外安装,我直接在Dockerfile里加了一行:
RUN apk add --no-cache curl
坑3:卷挂载权限问题
PostgreSQL数据卷一开始用的bind mount(./pgdata:/var/lib/postgresql/data),结果宿主机目录权限不对,容器启动报错。改成命名卷后彻底解决,Compose自动管理权限,而且docker volume ls能看到数据独立于容器生命周期。
优化:并行启动提速
Compose默认并行启动无依赖关系的服务。优化后,nginx、postgres、redis同时启动,backend等健康检查通过后启动。实测完整启动时间从串行的约400秒(含JVM启动和数据库初始化)降到120秒,减少了70%。
六、效果数据与验证
部署到4核8G机器后,我用docker compose ps观察状态:
NAME IMAGE STATUS
nginx-gateway nginx:1.25.3-alpine Up 2 hours (healthy)
backend-service myapp/backend:1.0.0 Up 2 hours (healthy)
postgres-db postgres:16.1-alpine Up 2 hours (healthy)
redis-cache redis:7.2.3-alpine Up 2 hours (healthy)
实际压测数据(wrk,100并发,30秒):
- 接口平均响应时间 86ms,P99 210ms
- 容器重启恢复:模拟docker compose restart backend,从停止到健康状态耗时约28秒(JVM启动20秒 + 健康检查8秒)
- 数据持久化验证:docker compose down后再up,PostgreSQL数据完整,Redis缓存键未丢失
七、总结与建议
这套配置已经稳定运行3周,部署效率提升明显。给几点建议:
- 网络一定要自定义,别用默认的bridge网络,服务名解析和隔离性都更可控。
- 健康检查别省,这是控制依赖序的关键,比任何
wait-for-it脚本都可靠。 - 生产环境务必用命名卷,数据安全是底线。
restart: always加上,配合健康检查,容器挂了能自动恢复,配合运维告警能做到无人值守。
下一步我准备把Compose配置迁移到Docker Swarm或K8s,但那是另一个故事了。有问题欢迎评论区交流。