一、为什么需要编排:手工部署的混乱与痛点
上个月接手一个老项目,四个服务:前端静态资源、后端API、PostgreSQL数据库、Redis缓存。之前都是手工部署,每次发版流程是这样的:
- 先启动数据库,等30秒确认能连上
- 再启动Redis,确认端口通
- 然后启动后端,等它注册到Nacos
- 最后配置Nginx反代
每次都要在多个终端来回切换,敲十几条docker run命令,还要记住各种-v、-p、--network参数。最崩溃的是有一次升级数据库镜像,忘了挂载数据卷,容器一删数据全没了。这还不算完,服务之间互相等待的时序问题反复出现,后端启动时数据库还没就绪,连接池报错。
这就是我写这篇文章的原因:用Docker Compose把整个部署流程代码化、可重复化。下面直接上干货。
二、环境与版本:不要再问为什么用旧版本
先说环境,别用什么奇奇怪怪的版本,以下是我验证过的组合:
- Docker Engine:24.0.7(2023年12月发布)
- Docker Compose:v2.24.2(通过docker-compose-plugin安装)
- 宿主机:Ubuntu 22.04 LTS,4核8G
- 镜像版本:nginx:1.25.3-alpine、openjdk:17-slim、postgres:16.1-alpine、redis:7.2.3-alpine
重要提醒:Compose V2已经内置在Docker CLI中,直接用docker compose(没有横杠)而非docker-compose。我见过太多人还在用老版本V1,语法都不兼容了。
三、方案设计:网络、卷、健康检查、启动顺序的决策
在设计编排方案时,我定了四条原则:
- 网络隔离:所有服务放在同一个自定义bridge网络中,不暴露数据库端口到宿主机(只有Nginx映射80端口)。这样数据库和Redis只在内部网络可达,安全性好。
- 数据持久化:PostgreSQL使用命名卷(named volume),Redis用AOF持久化,把appendonly配置挂载到匿名卷。
- 健康检查:每个服务都配healthcheck,确保依赖的服务真正可用,而不是端口能通就算成功。
- 启动顺序:使用
depends_on.condition: service_healthy,让Compose等待依赖服务健康后再启动后续服务。
四、核心实现:docker-compose.yml完整解析
先看完整配置,然后我逐个拆解:
version: "3.9"
services:
postgres:
image: postgres:16.1-alpine
container_name: blog-postgres
restart: unless-stopped
environment:
POSTGRES_DB: blogdb
POSTGRES_USER: bloguser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- blog_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bloguser -d blogdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
ports:
- "5432:5432" # 仅开发环境注释掉,生产环境删除此行
redis:
image: redis:7.2.3-alpine
container_name: blog-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
networks:
- blog_net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
ports:
- "6379:6379" # 同样,生产环境删除
backend:
build: ./backend
container_name: blog-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/blogdb
SPRING_DATASOURCE_USERNAME: bloguser
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
volumes:
- ./logs:/app/logs
networks:
- blog_net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 60s
nginx:
image: nginx:1.25.3-alpine
container_name: blog-nginx
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./frontend/dist:/usr/share/nginx/html:ro
networks:
- blog_net
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 10s
timeout: 5s
retries: 3
volumes:
pg_data:
driver: local
redis_data:
driver: local
networks:
blog_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
4.1 网络配置:为什么指定subnet
自定义网络默认桥接模式,但我手动指定了172.28.0.0/16,好处是固定网段,方便在防火墙或监控中精确匹配IP。而且服务名就是DNS名称,比如后端连接数据库直接用postgres:5432,不需要知道IP。
4.2 卷挂载:数据安全和日志管理
PostgreSQL的pg_data卷是命名的,即使docker compose down删除容器,数据依然保留。Redis同理。注意我没有把Redis的appendonly.aof单独挂载,而是整个/data目录,因为Redis会在这个目录下生成dump.rdb和appendonly.aof两个文件。
后端日志目录挂载到宿主机的./logs,方便用logrotate或ELK收集。Nginx配置和前端静态文件用bind mount方式,这样改配置不用重建镜像,nginx -s reload就行。
4.3 健康检查:别用telnet,用业务命令
很多人喜欢用nc -z localhost 5432或者curl检查端口,这在某些场景下会误判。比如PostgreSQL刚启动时端口可能已经监听,但实际还没准备好接受连接。PostgreSQL官方镜像自带pg_isready,Redis自带redis-cli ping,这才是正确的方式。Spring Boot的actuator health端点会检查数据库连接池,确保数据库连接可用。
4.4 启动顺序:depends_on的坑
Compose V2的depends_on默认只等待容器启动(started),不等待服务就绪。必须显式加上condition: service_healthy。我用的是:
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
这意味着backend容器会等到postgres和redis都健康后才启动。同理,nginx等到backend健康后才启动。
五、踩坑记录:三个真实问题与解决方案
5.1 坑一:健康检查超时导致服务永不启动
第一次配置时,我把PostgreSQL的start_period设成了5秒,结果容器启动慢(要初始化数据目录),5秒内没通过健康检查,Compose直接标记为unhealthy,backend永远等不到。解决方案是把start_period放宽到20秒,interval设为10秒,retries设为5。start_period不算在retries里,它是容错窗口。
5.2 坑二:Spring Boot启动太慢,Nginx等不起
Spring Boot 3.2启动约25秒,但我同时配置了数据库连接池初始化、Redis连接、定时任务预热,导致健康检查60秒后才通过。Nginx那边interval是10秒,retries是3次,30秒就宣告失败。后来把Nginx的depends_on也加上condition: service_healthy,并且将backend的start_period调至90秒,问题解决。
5.3 坑三:环境变量泄露
配置里有数据库密码,直接用明文写在yaml里。后来改成.env文件:
# .env
DB_PASSWORD=MyS3cureP@ssw0rd
然后在compose文件里用${DB_PASSWORD}引用。并且把.env加进.gitignore,避免上传到代码仓库。
5.4 优化:资源限制
给每个服务加了deploy.resources.limits,避免某个服务吃光内存:
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
实测加了限制后,整体内存占用从1.8G降至1.5G,性能影响几乎无感。
六、效果数据:部署效率对比
用这套Compose配置后,我在同一台机器上做了三次测试:
| 指标 | 手工部署 | Docker Compose |
|---|---|---|
| 首次部署时长 | 3分20秒 | 45秒(镜像已拉取) |
| 服务重启恢复 | 2分15秒 | 12秒 |
| 配置变更生效 | 需逐台操作 | 一行命令搞定 |
| 数据持久化失败率 | 曾发生数据丢失 | 0次 |
更重要的是,现在团队任何人拿到代码仓库,只要装好Docker,执行docker compose up -d就能获得一套完整可运行的开发环境。不是嘴上说说,是真的做到了基础设施即代码。
七、总结与建议
Docker Compose不是万能的,但对于中小型项目的容器化编排,它比Kubernetes简单太多,足够满足90%的场景。我的建议是:
- 每个服务都必须配置健康检查,这是编排可靠性的基石。
- 命名卷和bind mount要分清:数据库用命名卷,配置文件用bind mount。
- depends_on必须用condition: service_healthy,否则只是表面顺序。
- 善用.env文件管理敏感信息,别硬编码密码。
- 生产环境把数据库端口从ports中移除,只保留内部网络访问。
如果你还没有把项目容器化,或者还在手工docker run,强烈建议花半天时间迁移到Compose。这可能是你今年做的性价比最高的技术投资。有问题欢迎评论区交流,我看到了会回复。