Skip to content

Compose:多容器编排与健康检查 ​

单机跑多个容器,用 docker run 拼命令不可维护。Compose 用一个 YAML 描述整个栈,一条命令起停。

场景 ​

App + MySQL + Redis + Nginx 四个容器,要保证启动顺序(数据库先就绪)、共享网络、数据持久化、配置可切换环境。

一、最小 compose.yaml ​

yaml
name: shop

services:
  app:
    build: .
    ports:
      - "8080:8080"

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
bash
docker compose up -d
docker compose ps
text
NAME          IMAGE         COMMAND                  SERVICE   CREATED          STATUS          PORTS
shop-app-1    shop-app      "java -jar /app/app…"    app       10 seconds ago   Up 9 seconds    0.0.0.0:8080->8080/tcp
shop-mysql-1  mysql:8.0     "docker-entrypoint.s…"   mysql     10 seconds ago   Up 9 seconds    3306/tcp

不要写 version 字段

Compose V2 已经不需要 version: "3.8",写了会告警。文件名用 compose.yaml(推荐)或 docker-compose.yml。

二、生产可用的完整栈 ​

yaml
name: shop

services:
  mysql:
    image: mysql:8.0.40
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: shop
      TZ: Asia/Shanghai
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --innodb-buffer-pool-size=1G
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 40s
    networks: [backend]

  redis:
    image: redis:7.2-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    networks: [backend]

  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        JAR_FILE: target/app.jar
    image: shop-app:${APP_VERSION:-latest}
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    env_file:
      - .env
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis
      TZ: Asia/Shanghai
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2g
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/actuator/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 60s
    networks: [backend, frontend]

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      app:
        condition: service_healthy
    networks: [frontend]

volumes:
  mysql-data:
  redis-data:

networks:
  frontend:
  backend:
    internal: true

几个关键点:

配置作用
${VAR} / ${VAR:-default}从 .env 或 shell 环境插值
depends_on.condition: service_healthy等健康检查通过才启动,不是简单的"先启动"
networks.backend.internal: true该网络无外网出口,数据库不能主动连出去
deploy.resources.limitsCompose V2 在单机也生效(cpus / memory)
image: shop-app:${APP_VERSION}指定构建产物的 tag,避免每次都是 latest

.env ​

text
# .env  (不要提交到 git)
MYSQL_ROOT_PASSWORD=Ch4ngeMe_2026
REDIS_PASSWORD=R3dis_2026
APP_VERSION=1.4.2
bash
docker compose config --quiet && echo ok
docker compose config | head -30
text
ok

docker compose config 会把插值后的完整 YAML 打出来,上线前必跑——能提前发现变量没设、缩进错、锚点引用错。

密码写在 compose.yaml 里会被提交

.env 加进 .gitignore。生产环境更稳妥的是用 env_file 指向宿主机上的 /etc/shop/.env(不随代码走),或用 Docker secret(Swarm)/ 外部配置中心。

三、depends_on 的两种语义 ​

yaml
# 只保证启动顺序,不等就绪
depends_on:
  - mysql

# 等健康检查通过
depends_on:
  mysql:
    condition: service_healthy

第一种是坑:MySQL 容器 Up 了但还在初始化(mysqld 需要 20~40 秒),app 立刻连必然失败,然后 restart: unless-stopped 无限重试。

第二种必须给被依赖服务配 healthcheck,否则 Compose 报错 dependency failed to start: container is unhealthy(或直接不等)。

健康检查怎么写才靠谱:

服务检查命令
MySQLmysqladmin ping -h 127.0.0.1 -uroot -p$PASS
PostgreSQLpg_isready -U postgres
Redisredis-cli -a $PASS ping
Kafkakafka-broker-api-versions.sh --bootstrap-server localhost:9092
应用curl -f http://127.0.0.1:8080/actuator/health

mysqladmin ping 有个细节:MySQL 在初始化阶段也会响应 ping(用的是临时 server),所以更严格的做法是:

yaml
test: ["CMD-SHELL", "mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e 'SELECT 1' >/dev/null 2>&1"]

注意 $$:Compose 会把 $ 当变量插值,$$ 转义成字面 $,让 shell 在容器内展开。

四、常用命令 ​

bash
docker compose up -d              # 后台启动(有变更会重建)
docker compose up -d --build      # 重新构建镜像
docker compose ps                 # 状态
docker compose logs -f --tail 100 app
docker compose exec app sh        # 进容器
docker compose restart app
docker compose stop               # 停止,不删容器
docker compose down               # 停止并删除容器/网络
docker compose down -v            # 连卷一起删(危险)
docker compose pull               # 拉最新镜像
docker compose config             # 校验 + 展开

down -v 会删数据卷

docker compose down -v 删除 volumes: 里声明的命名卷,数据库跟着没。日常发布只用 down 或 up -d,永远不要带 -v。

只操作某个服务 ​

bash
docker compose up -d --no-deps --build app     # 只重建 app,不动依赖
docker compose logs -f app
docker compose scale app=3                      # 起 3 个实例(需要去掉固定端口映射)

scale 要求服务不写死宿主端口(ports: "8080:8080" 会冲突),要么去掉 ports 走 Nginx 上游,要么用端口段 "8080-8082:8080"。

五、多环境覆盖 ​

bash
docker compose -f compose.yaml -f compose.prod.yaml up -d

compose.prod.yaml:

yaml
services:
  app:
    ports: []                       # 覆盖为空,只让 nginx 访问
    environment:
      SPRING_PROFILES_ACTIVE: prod
  nginx:
    ports:
      - "80:80"
      - "443:443"

Compose 会深度合并:标量覆盖,列表替换(不是追加)。所以 ports: [] 能清掉基础文件里的端口。

也可以把公共部分抽到 compose.base.yaml,用 extends:

yaml
services:
  app:
    extends:
      file: compose.base.yaml
      service: app

六、profiles:按需启动可选服务 ​

yaml
services:
  app:
    image: shop-app:1.0.0

  adminer:
    image: adminer:latest
    ports: ["8081:8080"]
    profiles: ["debug"]

  prometheus:
    image: prom/prometheus
    profiles: ["monitor"]
bash
docker compose up -d                       # 只起 app
docker compose --profile debug up -d      # app + adminer
docker compose --profile debug --profile monitor up -d

带 profiles 的服务在未激活时完全被忽略——不会创建、不会出现在 ps 里。

七、Compose 的边界 ​

能做不能做
单机多容器编排跨主机调度
健康检查、重启策略自动扩缩容(scale 是静态的)
资源限制滚动发布(会短暂中断)
网络与卷管理密钥管理(只能靠文件挂载)

要跨机器、要滚动发布、要自动伸缩,就该上 K8s。Compose 的定位是单机部署,把它当轻量 K8s 用会撞墙。

八、踩过的坑 ​

现象根因处置
app 启动就连不上数据库depends_on 没配 condition加 service_healthy
dependency failed to start被依赖服务 unhealthy看它的 logs,放宽 start_period
改了代码但没生效用的是旧镜像up -d --build
port is already allocated宿主端口被占ss -lntp 查,改映射
${VAR} 变成空.env 不在 compose 同目录--env-file 指定,或放到一起
健康检查里的 $ 被吞Compose 插值写 $$
down -v 后数据库空了删了命名卷永远别带 -v
服务名解析不了不在同一 network检查 networks 声明
重建后容器 IP 变了正常现象用服务名,不要用 IP
日志文件巨大没配 log-opts见 资源与日志