一、问题背景:手动部署的混乱与救赎

上个月接了个电商后台项目,4个服务(前端Nginx、后端API、数据库PG、缓存Redis)部署在测试环境。刚开始图省事,写了个shell脚本挨个docker run,结果踩了一堆坑:

  1. 启动顺序靠sleep:脚本里sleep 10等数据库就绪,结果网络波动时API连不上库,直接崩溃
  2. 网络IP写死:容器重建后IP变了,Nginx配置里的proxy_pass http://172.17.0.3:8080就失效
  3. 数据说丢就丢:某次清理容器,数据库数据全没了,被测试同事骂了一下午

后来换成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_netbackend_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_oncondition在旧版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即可)

七、总结与建议

这套配置我用了三个月,稳定运行没出过幺蛾子。总结几个经验:

  1. 网络隔离别偷懒:双网络设计虽然多写几行配置,但安全收益值得。至少做到DB和Redis不暴露到外网
  2. 健康检查是刚需:不是可选项,特别是数据库和消息队列这类有启动时间的组件。start_period参数留足30秒,避免服务启动慢导致误判
  3. 卷挂载要规划:数据库数据卷用命名卷,配置文件用bind mount(方便修改)。日志目录bind mount到宿主机,方便收集
  4. 版本锁定:镜像tag用精确版本(如15.4-alpine),别用latest,否则某天拉个新版本可能不兼容

最后留个思考:这套配置在单机部署没问题,如果上K8s还得改造,但Compose的配置逻辑和网络设计思路是通用的。有问题欢迎评论区交流,老哥我看到了会回复。