一、为什么需要Compose编排?从一次凌晨2点的故障说起

上周五晚上,我们一个内部管理系统突然无法登录。排查发现是后端服务先于数据库启动,连接池抛异常后进入死循环重启。这种问题在单机部署时很少遇到,但在容器化环境里,启动顺序依赖检查成了必须处理的问题。

我们团队之前一直是手动docker run启动各服务,随着服务数量增加到6个,操作成本指数上升。而且不同服务的网络配置、环境变量、卷挂载分散在多个shell脚本里,没人能说清整体架构。这才下定决心迁移到Docker Compose统一编排。

本文不教基础语法,只分享一个可直接用于生产环境的三层应用编排方案,重点解决网络隔离、数据持久化、健康检查和启动顺序这四个核心痛点。

二、环境与版本说明

在开始之前,先明确我使用的环境版本(版本一致性是排查问题的第一步):

Docker Engine: 24.0.7
Docker Compose: v2.23.3
Linux Kernel: 5.15.0 (Ubuntu 22.04)

服务组件版本:
- Nginx: 1.25.3 (alpine镜像,体积仅23MB)
- Spring Boot: 3.1.5 (JDK 17,打包为可执行jar)
- PostgreSQL: 16.1 (使用官方镜像,内置健康检查脚本)

如果你的版本低于Docker Compose v2.20,部分healthcheck参数(如start_period)可能不支持,建议先升级。

三、方案设计:三层架构的网络拓扑

我们的应用是典型的前后端分离架构:Nginx作为反向代理处理静态资源和请求转发,Spring Boot提供REST API,PostgreSQL存储业务数据。

核心设计决策:

  1. 双网络隔离:创建两个自定义桥接网络——frontend_netbackend_net。Nginx同时接入两个网络,但只有Nginx能访问后端服务,数据库完全隔离在外网,只对后端服务可见。这比默认桥接网络更安全。

  2. 命名卷挂载:PostgreSQL数据目录使用命名卷pg_data,即使容器重建数据也不会丢失。后端日志目录挂载到宿主机./logs,方便排查问题。

  3. 健康检查三层递进:每个服务都配置healthcheck,但策略不同——数据库用pg_isready命令,后端用Spring Boot Actuator的/actuator/health端点,Nginx检查/health路由的HTTP状态码。

  4. 启动顺序组合拳:光用depends_on是不够的,必须配合condition: service_healthy。这样后端会等数据库健康后再启动,Nginx会等后端健康后再启动,形成严格的依赖链。

四、核心实现:完整docker-compose.yml配置

下面是完整的配置文件,我加了详细的注释说明每个参数的作用。请直接复制使用,但记得替换其中的环境变量为你的实际值。

version: "3.8"

# ========== 命名卷声明 ==========
volumes:
  pg_data:                # 数据库持久化卷
    driver: local
  nginx_logs:             # Nginx日志卷

# ========== 自定义网络声明 ==========
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

services:
  # ---------- 1. PostgreSQL 数据库 ----------
  db:
    image: postgres:16.1-alpine
    container_name: app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}    # 从.env文件读取,不要明文写入
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro   # 初始化SQL脚本
    networks:
      - backend_net          # 只接入后端网络,不暴露到宿主机
    ports:
      - "127.0.0.1:5432:5432"    # 仅本机可访问,用于开发调试
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d myapp"]
      interval: 10s          # 每10秒检查一次
      timeout: 5s            # 单次检查超时5秒
      retries: 5             # 连续5次失败则标记为unhealthy
      start_period: 30s      # 容器启动后宽限30秒,避免启动初期误判
    deploy:
      resources:
        limits:
          memory: 1G         # 限制内存防止OOM

  # ---------- 2. Spring Boot 后端服务 ----------
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: myapp-backend:1.0.0
    container_name: app-backend
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy    # 关键:等待数据库健康后才启动
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
      SPRING_DATASOURCE_USERNAME: app_user
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      SPRING_PROFILES_ACTIVE: prod
      SERVER_PORT: 8080
    volumes:
      - ./logs:/app/logs          # 挂载日志目录到宿主机
    networks:
      - backend_net                # 只与数据库通信
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 40s           # Spring Boot启动较慢,需要更长宽限时间
    deploy:
      resources:
        limits:
          memory: 1.5G
          cpus: "1.0"

  # ---------- 3. Nginx 反向代理 ----------
  nginx:
    image: nginx:1.25.3-alpine
    container_name: app-nginx
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_healthy    # 确保后端已就绪
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./static:/usr/share/nginx/html:ro    # 前端静态文件
      - nginx_logs:/var/log/nginx
    networks:
      - frontend_net
      - backend_net                # 同时接入两个网络,作为网关
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 10s

五、踩坑记录与优化方案

坑1:depends_on不是万能的

最初我只写了depends_on: - db,结果后端容器启动时数据库还在初始化,连接池报错。换成condition: service_healthy后问题解决。但注意:健康检查本身有延迟,所以start_period必须设置足够大。PostgreSQL首次初始化可能需要20-30秒,我设置了30秒。

坑2:healthcheck命令的选择

后端我用的是curl检查Actuator端点,但基础镜像(如eclipse-temurin)里没有curl。解决方案是在Dockerfile里安装curl,或者改用Java自带的JAVA_OPTS方式。我更推荐在基础镜像中预装curl,因为调试方便。Nginx镜像自带wget,所以用wget。

坑3:网络子网冲突

默认的bridge网络子网是172.17.0.0/16,如果宿主机已有VPN或虚拟网卡占用,会冲突。显式指定子网后彻底解决。

坑4:卷权限问题

PostgreSQL容器以postgres用户运行,但宿主机挂载目录权限可能不匹配。解决方案是使用命名卷(如pg_data),数据由容器内用户管理,宿主机只负责存储。不要挂载宿主机目录到数据库数据路径。

优化方案:

  • 环境变量管理:使用.env文件存储敏感信息,docker-compose自动读取。.env文件加入.gitignore
  • 日志轮转:Nginx和应用的日志如果不轮转,一个月能占满磁盘。在compose里加上logging配置:
logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

六、效果数据与总结

迁移到Compose编排后,我们对部署流程做了计时对比:

指标 手动脚本部署 Docker Compose
部署时间 15分钟(含等待就绪) 2分10秒
启动失败率 约12%(顺序问题) 0.3%
回滚时间 约8分钟(手动处理) 30秒(docker compose down + up)
服务可用性 99.2% 99.95%

核心收益: 配置即代码,整个应用架构在一个文件中清晰可见。新增开发环境只需复制compose文件并调整端口映射,团队成员不再需要读冗长的shell脚本。

几个建议:
1. 生产环境务必使用docker compose config校验配置正确性。
2. 结合docker compose scale横向扩展后端服务(但需要处理会话共享)。
3. 定期执行docker compose ps检查各服务健康状态,配合cron做自动告警。

最后说一句:容器编排不是银弹,但正确使用Compose能让你的部署流程从“玄学”变成“科学”。有问题欢迎在评论区交流。