1. 问题背景:为什么单体部署撑不住了?
我们团队维护一个中型电商后台系统,最初用Shell脚本启动四个进程:Nginx、Spring Boot应用、PostgreSQL和Redis。痛点非常明显——每次发版都要手动SSH到服务器,敲一串nohup java -jar xxx.jar &,然后祈祷端口没被占用。最崩溃的一次是Redis没起来,但Spring Boot正常启动,结果缓存穿透把数据库打挂了,凌晨两点被电话叫醒。
后来我们决定全面容器化,但光用docker run一个个启动也不行,因为服务之间有严格的依赖关系:PostgreSQL和Redis必须先就绪,Spring Boot才能连上;Nginx要等Spring Boot监听端口后才能代理请求。这就需要一套编排工具来管理生命周期——Docker Compose正好干这个。
2. 环境与版本说明
先交代一下生产环境配置,避免版本不一致导致踩坑:
- Docker Engine:24.0.7(支持
depends_on.condition语法) - Docker Compose:v2.24.0(推荐使用
docker compose插件版,而不是老旧的docker-compose) - 宿主机:Ubuntu 22.04 LTS,4核8G内存
- 镜像版本:nginx:1.25.3-alpine、postgres:15.4-alpine、redis:7.2-alpine、openjdk:17-jdk-alpine
注意一点:Compose V2对depends_on的condition支持更完善,如果还在用V1(Python版),很多新特性用不了。
3. 方案设计:网络、卷、健康检查的协同
核心设计思路有三条:
第一,网络隔离。 使用自定义Bridge网络,而不是默认的default网络。这样服务间通过服务名通信(DNS解析),并且可以设置internal: true让数据库和缓存不暴露到宿主机端口,只允许应用容器访问。
第二,数据持久化。 PostgreSQL和Redis的数据必须落到命名卷里。注意用命名卷而不是bind mount(绑定挂载),因为bind mount受宿主机目录权限影响大,在CI/CD流水线里容易出幺蛾子。
第三,启动顺序和故障恢复。 这是最容易忽略的点。Spring Boot启动时如果连不上数据库,DataSource初始化会抛异常直接退出。所以必须让Compose等待PostgreSQL的pg_isready命令成功返回,Redis的redis-cli ping返回PONG,然后才启动业务容器。此外,healthcheck还能让Compose自动重启不健康的容器。
4. 核心实现:docker-compose.yml完整配置
直接上配置文件,这是生产环境在用的精简版,删掉了敏感信息:
version: "3.9"
services:
# ---------- 反向代理 ----------
nginx:
image: nginx:1.25.3-alpine
container_name: gateway-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- web-static:/usr/share/nginx/html:ro
networks:
- frontend
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
# ---------- 业务应用 ----------
app:
build:
context: ./backend
dockerfile: Dockerfile
image: mall-backend:1.4.2
container_name: mall-app
expose:
- "8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: mall_db
DB_USER: mall_user
DB_PASSWORD: ${DB_PASSWORD:-change_me}
REDIS_HOST: redis
REDIS_PORT: 6379
JVM_OPTS: "-Xms512m -Xmx1g"
volumes:
- app-logs:/logs
- ./backend/config:/config:ro
networks:
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 20s
timeout: 3s
retries: 5
start_period: 40s
restart: unless-stopped
# ---------- 数据库 ----------
postgres:
image: postgres:15.4-alpine
container_name: mall-postgres
environment:
POSTGRES_DB: mall_db
POSTGRES_USER: mall_user
POSTGRES_PASSWORD: ${DB_PASSWORD:-change_me}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- pg-data:/var/lib/postgresql/data
- ./init-sql:/docker-entrypoint-initdb.d:ro
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mall_user -d mall_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# ---------- 缓存 ----------
redis:
image: redis:7.2-alpine
container_name: mall-redis
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-redis_pass}"]
volumes:
- redis-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_pass}", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
# ---------- 网络定义 ----------
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend:
driver: bridge
internal: true # 关键:后端网络不映射宿主机端口
ipam:
config:
- subnet: 172.21.0.0/24
# ---------- 卷定义 ----------
volumes:
pg-data:
driver: local
redis-data:
driver: local
app-logs:
driver: local
web-static:
driver: local
5. 踩坑与优化:三个血泪教训
坑一:depends_on不写condition等于白写。 最初我只写了depends_on: [postgres, redis],以为会等待健康检查。结果Compose只保证启动顺序,不保证服务就绪。Spring Boot容器启动时数据库还没初始化完,直接报Connection refused。后来加上condition: service_healthy才解决。注意:这个语法要求Compose V2.1+,老版本不支持。
坑二:PostgreSQL的数据目录卷挂载冲突。 官方镜像默认数据目录是/var/lib/postgresql/data,但如果你同时挂载了卷和设置PGDATA环境变量,会有权限问题(PostgreSQL容器以非root用户运行)。我们的解决方案是设置PGDATA: /var/lib/postgresql/data/pgdata,并把卷挂载到/var/lib/postgresql/data,这样卷的所有权归容器用户,不会报chmod错误。
坑三:healthcheck的start_period设置太短。 Spring Boot首次启动需要加载JVM、初始化连接池,在4核8G机器上大约30秒左右。如果start_period设成5秒,healthcheck会在JVM还没起来时就执行curl,直接标记为unhealthy然后重启容器,导致永远起不来。我调成40s后稳定了。这个参数的含义是:在start_period内,健康检查失败不计入重试次数。
优化点方面,我做了两个动作:
一是把internal: true加到backend网络,让PostgreSQL和Redis完全不对宿主机暴露端口。安全隐患直接消除,因为之前有扫描工具发现5432端口公网可达。
二是用restart: unless-stopped配合healthcheck,实现故障自愈。实测当Redis容器被kill后,Compose在8秒内自动重建容器,且Spring Boot连接池自动重连,对业务零感知。
6. 效果数据:对比部署效率
改造前后对比(同一台4核8G服务器,冷启动):
| 指标 | 脚本部署 | Docker Compose |
|---|---|---|
| 部署耗时(首次) | 约15分钟(含装环境) | 3分20秒(镜像拉取后) |
| 服务启动时间 | 47秒(串行等待) | 23秒(并行启动+健康检查) |
| 回滚速度 | 手动替换jar包,5分钟 | 镜像tag切换,<1分钟 |
| 故障恢复时间 | 人工介入,10分钟+ | 自动重启,8秒内 |
| 环境一致性 | 经常"我这能跑" | 完全一致 |
另外,因为用了命名卷,数据库升级时只要docker compose up -d --no-deps postgres,数据不会丢失。之前用bind mount被权限搞疯过的同学应该懂这个有多爽。
7. 总结与建议
Docker Compose编排的核心不是写YAML,而是理解服务间的依赖语义。关键就三点:
- 网络划分:前端网络和后端网络隔离,不必要暴露的端口一律不暴露。
- 健康检查是灵魂:没有healthcheck,
depends_on只是摆设。写清楚test、interval、start_period,Compose才能真正"智能"。 - 卷要命名:数据卷用命名卷,不要用
./data这种bind mount,否则容器重建后权限和一致性问题让你欲哭无泪。
最后提一句:如果服务规模超过10个,或者需要跨主机编排,可以考虑K8s或Docker Swarm。但中短期项目,Docker Compose完全够用,别过度设计。有问题欢迎评论区交流,看到都会回。