1. 问题背景:docker run脚本失控了
先交代下背景。我维护的一个内部工具链,包含网关(Nginx)、后端(Spring Boot)、数据库(PostgreSQL)和缓存(Redis)。最初部署方式是写了一个deploy.sh,里面堆了七八条docker run命令。随着版本迭代,问题逐渐暴露:
- 启动顺序不可控:后端容器启动时如果连不上数据库,Spring Boot会直接退出,导致整个服务不可用。
- 网络配置混乱:容器之间通过
--link或暴露端口通信,IP地址在重启后变化,配置文件里的IP写死导致频繁报错。 - 数据持久化靠挂载宿主机目录:不同机器路径不一致,迁移困难。
最崩溃的一次是线上排查问题,发现Redis容器因为内存溢出重启了三次,但日志里完全没有记录——因为没有设置restart策略,容器挂了就挂了。
2. 环境与版本
本次使用的环境:
- Docker Engine: 26.1.3(支持Compose v2)
- Docker Compose: v2.27.0
- 镜像版本:
- Nginx: 1.27-alpine
- Spring Boot: 基于
eclipse-temurin:17-jre-alpine构建的自定义镜像 - PostgreSQL: 16.3-alpine
- Redis: 7.2.5-alpine
所有配置文件已上传至GitLab仓库,团队内共享。
3. 方案设计:为什么选择Compose而非K8s
有人可能问,为什么不用Kubernetes?原因很简单:这个项目只有4个服务,K8s的运维成本(节点管理、Ingress配置、RBAC)远超收益。Compose v2已经支持依赖条件、健康检查和自动重启,够用了。
设计原则:
- 网络隔离:创建两个网络——
frontend_net(Nginx和后端通信)和backend_net(后端连接数据库和Redis)。Nginx不能直接访问数据库,减少攻击面。 - 命名卷持久化:PostgreSQL数据目录和Redis持久化目录用命名卷,不依赖宿主机路径。
- 健康检查驱动启动顺序:通过
depends_on+condition: service_healthy,确保数据库和Redis完全就绪后再启动后端。 - 资源限制:给每个容器设置
mem_limit和cpus,防止Redis内存溢出拖垮宿主机。
4. 核心实现:docker-compose.yml详解
直接上配置,关键部分都有注释。
version: "3.8"
services:
nginx:
image: nginx:1.27-alpine
container_name: api-gateway
ports:
- "8080:80"
networks:
- frontend_net
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
restart: unless-stopped
mem_limit: 128m
cpus: "0.5"
app:
build: ./backend
image: my-backend:1.0.0
container_name: spring-app
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
SPRING_DATA_REDIS_HOST: redis
SERVER_PORT: 8080
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
mem_limit: 512m
cpus: "1.0"
db:
image: postgres:16.3-alpine
container_name: postgres-db
environment:
POSTGRES_DB: mydb
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从.env文件读取
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d mydb"]
interval: 5s
timeout: 3s
retries: 5
start_period: 15s
restart: unless-stopped
mem_limit: 512m
cpus: "1.0"
redis:
image: redis:7.2.5-alpine
container_name: redis-cache
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
networks:
- backend_net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
restart: unless-stopped
mem_limit: 300m
cpus: "0.5"
networks:
frontend_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend_net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24
volumes:
pgdata:
name: pgdata_vol
redisdata:
name: redisdata_vol
关键配置解读:
depends_on配合condition: service_healthy:这是Compose v2的核心特性。db和redis的healthcheck通过后,app才会启动。之前版本只有depends_on,只能保证容器创建顺序,不能保证服务就绪。- 健康检查的
start_period参数:给容器初始化留出时间,避免启动慢导致误判。 - 网络子网固定:虽然Compose会自动分配子网,但固定IP段便于防火墙规则和调试。
restart: unless-stopped:配合mem_limit,容器OOM被杀后会自动重启,避免服务中断。
5. 踩坑与优化:三个血泪教训
教训一:Spring Boot的数据库连接重试配置
即使有了depends_on,Spring Boot启动时也可能遇到数据库连接池初始化失败。原因是healthcheck通过只能说明PostgreSQL进程活着,但可能还在恢复数据。解决方案是在application.yml中配置连接重试:
spring:
datasource:
hikari:
connection-timeout: 30000
initialization-fail-timeout: 60000
jpa:
properties:
javax.persistence.schema-generation.create-database-schemas: false
同时增加spring.boot.actuator.health.db.enabled=true,让healthcheck能反映真实的数据库连接状态。
教训二:Nginx健康检查不能用wget
Nginx的官方镜像基于Alpine,没有wget。我用的是wget,但在nginx:1.27-alpine中,wget是BusyBox版,不支持--spider参数。解决方案是改用curl——但Alpine镜像也没装curl。最终方案是:
# 在Dockerfile中安装curl
RUN apk add --no-cache curl
或者在compose中覆盖healthcheck命令,用[ "CMD", "sh", "-c", "curl -f http://localhost/healthz" ],但前提是镜像里有curl。
教训三:Redis内存限制必须显式设置
之前Redis容器没设置maxmemory,结果在压力测试时内存涨到宿主机的80%,触发内核OOM killer。加了--maxmemory 256mb --maxmemory-policy allkeys-lru后,Redis会主动淘汰数据而不是崩溃。同时配合mem_limit: 300m,给Redis留出50MB的缓冲区,避免OOM误杀。
6. 效果数据:迁移前后的对比
我做了两组测试,用同一份代码,分别在docker run脚本和docker-compose下运行:
| 指标 | docker run脚本 | docker-compose |
|---|---|---|
| 冷启动总耗时 | 42秒 | 18秒 |
| 服务可用时间 | 55秒(要等脚本轮询) | 20秒(healthcheck通过即可用) |
| 容器重启次数(48小时) | 3次(Redis OOM) | 0次 |
| 内存占用(全部容器) | 1.42GB | 1.09GB |
| 配置行数 | 87行(deploy.sh) | 64行(compose.yml) |
内存下降23%,主要是因为Compose的mem_limit限制了JVM的堆外内存膨胀,以及PostgreSQL的共享缓冲区被限制在合理范围。
7. 总结与推荐做法
Docker Compose v2已经足够应对中小型项目的容器编排需求。我的建议是:
- 健康检查必须是服务级别的,不要只检查进程存活,要检查端口是否可访问。
- 启动顺序用
condition: service_healthy,不要用depends_on的默认行为。 - 所有容器设置
restart: unless-stopped,配合资源限制,避免OOM后服务永久死亡。 - 网络隔离是必须的,至少分成前端访问层和后端数据层两个网络。
如果你还在用shell脚本管理容器,可以试试迁移到Compose。配置上手成本不高,但带来的稳定性和可维护性提升是实打实的。下次项目上线,我会把Compose配置作为CI流水线的一环,与代码一起版本化。