一、问题背景:手动部署的混乱与救赎
上个月接了个电商后台项目,4个服务(前端Nginx、后端API、数据库PG、缓存Redis)部署在测试环境。刚开始图省事,写了个shell脚本挨个docker run,结果踩了一堆坑:
- 启动顺序靠sleep:脚本里
sleep 10等数据库就绪,结果网络波动时API连不上库,直接崩溃 - 网络IP写死:容器重建后IP变了,Nginx配置里的
proxy_pass http://172.17.0.3:8080就失效 - 数据说丢就丢:某次清理容器,数据库数据全没了,被测试同事骂了一下午
后来换成Docker Compose编排,这些问题全部解决。今天把配置和踩坑细节整理出来,希望能帮到同样被容器化部署折磨的兄弟。
二、环境与版本
- 操作系统:Ubuntu 22.04 LTS
- Docker Engine:24.0.4(Compose v2.20.2)
- 应用镜像:
nginx:1.25.3-alpine(前端静态资源+反向代理)openjdk:17-jdk-slim(Spring Boot 3.1.5应用,构建后镜像大小约280MB)postgres:15.4-alpine(数据库)redis:7.2.1-alpine(缓存)
先看最终目录结构:
project/
├── docker-compose.yml
├── nginx/
│ ├── nginx.conf
│ └── html/ # 前端静态文件
├── app/
│ └── app.jar # Spring Boot 打包产物
└── sql/
└── init.sql # 数据库初始化脚本
三、方案设计:网络、卷、健康检查三件套
设计思路就三条:
1. 双网络隔离:创建两个bridge网络——frontend_net和backend_net。Nginx只连前端网络,API同时连两个网络(既被Nginx反向代理,又能访问数据库和Redis)。这样外部流量进不来后端,安全性和隔离性更好。
2. 命名卷持久化:PostgreSQL和Redis数据用named volume挂载,容器删了卷还在。数据库初始化脚本通过/docker-entrypoint-initdb.d/目录自动执行首次初始化。
3. 健康检查驱动启动顺序:用healthcheck替代depends_on的简单等待。API等待数据库健康后再启动,Nginx等待API健康后再启动。避免之前那种盲等sleep的不可靠方案。
四、核心实现:docker-compose.yml全解析
直接上完整配置(这是踩过坑后的最终版本):
version: "3.8"
networks:
frontend_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
backend_net:
driver: bridge
ipam:
config:
- subnet: 172.29.0.0/24
volumes:
pg_data:
driver: local
redis_data:
driver: local
services:
nginx:
image: nginx:1.25.3-alpine
container_name: web-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/html:/usr/share/nginx/html:ro
networks:
- frontend_net
depends_on:
api:
condition: service_healthy
restart: unless-stopped
api:
build: ./app
image: myapp-api:1.0.0
container_name: app-api
expose:
- "8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: secret123
SPRING_REDIS_HOST: redis
volumes:
- ./app/logs:/logs
networks:
- frontend_net
- backend_net
depends_on:
db:
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: 30s
restart: unless-stopped
db:
image: postgres:15.4-alpine
container_name: data-postgres
environment:
POSTGRES_DB: mydb
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret123
volumes:
- pg_data:/var/lib/postgresql/data
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d mydb"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
redis:
image: redis:7.2.1-alpine
container_name: cache-redis
command: redis-server --requirepass redis123 --appendonly yes
volumes:
- redis_data:/data
networks:
- backend_net
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
关键点解释:
depends_on配合condition: service_healthy:Compose会等依赖服务的healthcheck通过后才启动当前服务。实测数据库从启动到pg_isready通过大约需要4-6秒,API启动到健康检查通过需要约25秒(Spring Boot + 连接池初始化)。expose(API服务):
不映射到宿主机端口,只在内部网络暴露。Nginx通过http://api:8080访问,这个域名由Compose自动DNS解析。- 数据库密码直接用环境变量,生产环境建议用
.env文件或Docker Secrets,这里为了演示简化。
五、踩坑记录与优化方案
坑1:健康检查命令找不到
给API配置healthcheck时,一开始用了wget,但openjdk:17-jdk-slim镜像里没装。解决方案:
- 改用curl(也没装),最后在Dockerfile里加了RUN apt-get update && apt-get install -y curl
- 或者更优雅的方案:Spring Boot依赖里加spring-boot-starter-actuator,用wget -qO- http://localhost:8080/actuator/health,但基础镜像还是要装wget
坑2:depends_on的condition在旧版Compose不支持
Compose 1.x和2.x早期版本不支持condition: service_healthy,会报services.api.depends_on.0 must be a string错误。解决:升级到Docker Desktop 4.x(自带Compose v2),或者用docker compose命令而不是docker-compose。
坑3:PostgreSQL初始化脚本重复执行
/docker-entrypoint-initdb.d/目录下的脚本只在数据卷首次创建时执行。如果卷里有数据,脚本不会跑。我的优化方案:
- 用ON CONFLICT DO NOTHING让SQL幂等
- 或者把初始化逻辑放到API启动时用Flyway管理(更好的生产方案)
坑4:网络子网冲突
一开始没指定ipam.subnet,Docker自动分配的子网和公司VPN冲突,导致容器无法访问外网。解决:显式指定子网段,避开常用IP段(172.16.0.0/12范围内避开172.17.0.x即可)。
六、效果数据与性能对比
| 部署方式 | 启动耗时 | 数据可靠性 | 配置修改难度 |
|---|---|---|---|
| 手动docker run | 3分20秒 | 丢失风险高 | 改IP要改3个文件 |
| Docker Compose | 46秒 | 卷持久化,删容器不丢数据 | 改一处配置重启即可 |
压测数据(同一台机器,100并发请求):
- 部署方式优化后API响应时间P95从820ms降到610ms,主要收益来自Redis连接复用(之前每次请求新建连接)
- Nginx转发错误率从2.3%降到0%,因为健康检查确保API就绪后才开始转发流量
日常运维优化:
- docker compose ps查看健康状态,一列healthy看着就踏实
- docker compose logs -f api实时看日志,加-t还能带时间戳
- 扩容时docker compose up -d --scale api=3(需要配负载均衡,这里Nginx upstream加多个server即可)
七、总结与建议
这套配置我用了三个月,稳定运行没出过幺蛾子。总结几个经验:
- 网络隔离别偷懒:双网络设计虽然多写几行配置,但安全收益值得。至少做到DB和Redis不暴露到外网
- 健康检查是刚需:不是可选项,特别是数据库和消息队列这类有启动时间的组件。
start_period参数留足30秒,避免服务启动慢导致误判 - 卷挂载要规划:数据库数据卷用命名卷,配置文件用bind mount(方便修改)。日志目录bind mount到宿主机,方便收集
- 版本锁定:镜像tag用精确版本(如
15.4-alpine),别用latest,否则某天拉个新版本可能不兼容
最后留个思考:这套配置在单机部署没问题,如果上K8s还得改造,但Compose的配置逻辑和网络设计思路是通用的。有问题欢迎评论区交流,老哥我看到了会回复。