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已经支持依赖条件、健康检查和自动重启,够用了。

设计原则:

  1. 网络隔离:创建两个网络——frontend_net(Nginx和后端通信)和backend_net(后端连接数据库和Redis)。Nginx不能直接访问数据库,减少攻击面。
  2. 命名卷持久化:PostgreSQL数据目录和Redis持久化目录用命名卷,不依赖宿主机路径。
  3. 健康检查驱动启动顺序:通过depends_on + condition: service_healthy,确保数据库和Redis完全就绪后再启动后端。
  4. 资源限制:给每个容器设置mem_limitcpus,防止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已经足够应对中小型项目的容器编排需求。我的建议是:

  1. 健康检查必须是服务级别的,不要只检查进程存活,要检查端口是否可访问。
  2. 启动顺序用condition: service_healthy,不要用depends_on的默认行为。
  3. 所有容器设置restart: unless-stopped,配合资源限制,避免OOM后服务永久死亡。
  4. 网络隔离是必须的,至少分成前端访问层和后端数据层两个网络。

如果你还在用shell脚本管理容器,可以试试迁移到Compose。配置上手成本不高,但带来的稳定性和可维护性提升是实打实的。下次项目上线,我会把Compose配置作为CI流水线的一环,与代码一起版本化。