一、从手动部署到编排的痛点
之前维护一个内部管理系统,包含Nginx反向代理、Spring Boot后端和PostgreSQL数据库。每次部署都要手动执行docker run命令,还得记住端口映射、环境变量,最痛苦的是数据库容器重启后数据丢失。有次升级后端代码,忘了先启动数据库,结果应用启动失败,排查了半小时才发现是连接超时。
后来决定用Docker Compose解决这些问题。我的需求很明确:
- 三个容器能一起启动、一起停止
- 数据库数据必须持久化
- 应用要等数据库就绪后再启动
- 容器间通信不用暴露端口到宿主机
二、环境与版本说明
我用的版本组合经过多次验证,稳定兼容:
- Docker Engine: 24.0.7
- Docker Compose: v2.23.3
- PostgreSQL镜像: postgres:15.4-alpine(90MB,比debian版小300MB)
- Spring Boot镜像: openjdk:17-slim + 项目jar包
- Nginx镜像: nginx:1.25.2-alpine
宿主机是Ubuntu 22.04 LTS,4核8G配置。这个组合在生产环境跑了4个月,没出过兼容问题。
三、网络与存储方案设计
网络方案我选择了自定义bridge网络而不是默认网络,原因是:
1. 默认网络不支持容器间DNS解析(虽然新版支持了,但自定义更清晰)
2. 可以控制哪些服务能互相访问
存储方面,三个挂载点:
- pg_data 卷:存储PostgreSQL数据文件
- ./nginx/conf.d 绑定挂载:实时修改Nginx配置
- ./logs 绑定挂载:收集应用日志
健康检查的设计是关键。PostgreSQL用pg_isready命令检查,Spring Boot用HTTP请求/actuator/health,Nginx检查/health端点。
四、核心实现:docker-compose.yml详解
先看完整配置,然后逐段解释关键部分:
version: '3.8'
networks:
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
volumes:
pg_data:
driver: local
services:
db:
image: postgres:15.4-alpine
container_name: app_db
restart: unless-stopped
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret123}
POSTGRES_DB: app_db
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- pg_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
networks:
backend:
aliases:
- db_host
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
deploy:
resources:
limits:
memory: 512M
app:
build: ./backend
image: myapp-backend:1.2.0
container_name: app_backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db_host:5432/app_db
SPRING_DATASOURCE_USERNAME: app_user
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-secret123}
volumes:
- ./logs:/app/logs
- ./uploads:/app/uploads
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 768M
nginx:
image: nginx:1.25.2-alpine
container_name: app_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./frontend/dist:/usr/share/nginx/html:ro
depends_on:
app:
condition: service_healthy
networks:
- backend
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
五、关键配置的踩坑与优化
坑1:健康检查命令的差异
PostgreSQL的pg_isready在容器里默认不在PATH中,我第一版写的test: ["CMD", "pg_isready"...]直接报错。后来改用CMD-SHELL并加上完整路径,或者用shell执行方式。最终用CMD-SHELL配合pg_isready -U app_user -d app_db,注意要指定用户和数据库名。
坑2:启动顺序的假象
depends_on如果不加condition: service_healthy,只会等容器启动,不会等服务就绪。我踩过这个坑:应用容器启动了,但数据库还在初始化,Spring Boot连接失败重启。加上健康检查条件后,问题解决。注意start_period参数很关键,给服务留出初始化时间,避免误判。
坑3:卷挂载的权限问题
PostgreSQL容器默认用postgres用户(UID 999)运行,挂载到宿主机的pg_data卷如果权限不对,容器启动会失败。我的解决方法是手动创建目录并设置权限:
mkdir -p /var/lib/docker/volumes/app_pg_data/_data
chown -R 999:999 /var/lib/docker/volumes/app_pg_data/_data
或者更简单,在compose文件里加user: "999:999"。
优化:网络别名(aliases)的使用
我给db服务加了aliases: db_host,这样应用连接数据库时用db_host而不是db,好处是即使容器名变了,应用代码不用改。实际上Compose网络默认支持服务名解析,但显式声明更清晰。
资源限制的实践
用deploy.resources.limits限制内存,防止某个容器占用全部内存。实际测试:Spring Boot容器限制768M后,GC表现更稳定,没有出现过OOM。PostgreSQL限制512M对中小项目够用。
六、性能数据与运维效果
改造后我做了对比测试:
| 指标 | 手动部署 | Compose编排 |
|---|---|---|
| 全量启动时间 | 45秒 | 18秒 |
| 单容器重启恢复 | 15秒 | 5秒 |
| 数据持久化 | 需要手动备份 | 自动卷存储 |
| 部署命令数 | 6条 | 1条 |
最明显的变化是排查问题的成本降低了。以前要检查各容器网络通不通,现在统一在一个网络里,DNS解析自动处理好。健康检查的状态通过docker compose ps就能看到:
NAME SERVICE STATUS PORTS
app_db db Up 12 minutes (healthy) 5432/tcp
app_backend app Up 10 minutes (healthy) 8080/tcp
app_nginx nginx Up 10 minutes (healthy) 0.0.0.0:80->80/tcp
有一次服务器重启,所有容器自动恢复,数据库数据完好,整个恢复时间不到30秒。这在以前是不可想象的——手动部署时我至少需要10分钟来检查各个服务状态。
七、总结与建议
这套配置已经稳定运行4个月,中间经历过3次代码更新、2次数据库迁移。总结几点经验:
- 健康检查一定要配置,特别是依赖链上的服务。
start_period要按服务启动时间来设置,太短会误报,太长会拖延故障发现。 - 卷挂载优先级:数据库数据用命名卷,配置文件用绑定挂载。命名卷方便迁移和备份,绑定挂载方便调试。
- 敏感信息用环境变量,我用的
.env文件配合${DB_PASSWORD:-secret123}语法,默认值只在开发环境使用。 - 版本锁定很重要,镜像tag不要用
latest,生产环境必须指定具体版本。
如果你刚开始用Compose,建议从我这个配置改起,去掉不需要的服务,加上自己的业务逻辑。遇到问题先看docker compose logs,大部分错误都是配置语法或路径问题。最后提醒一句:docker compose config命令可以校验配置文件的语法,写完先跑一下这个,能省很多排查时间。