Docker Compose 编排多服务

一个稍具规模的应用至少有三个进程:应用、数据库、缓存,再加一个 Nginx 就是四个。手工 docker run 四次,参数会越来越长,容器之间的启动顺序还得靠人盯。Compose 用一个 YAML 文件描述整套服务,up -d 一次拉起,这才是单机部署的正确姿势。

Compose v1 与 v2 的区别

维度docker-compose v1docker compose v2
形态独立的 Python 程序Docker CLI 插件
命令docker-compose up -ddocker compose up -d
安装pip install docker-compose随 Docker Engine 一起装
状态2023 年 7 月起停止维护当前唯一推荐
兼容性version: "3.8" 必须写顶层 version 已废弃,写了会告警

新项目一律用 docker compose(中间有空格);见到以 docker-compose 单命令为主角的教程,多半是三年前的资料。

Compose 文件结构

顶层键作用常见配置
name项目名,决定容器名前缀name: tools-site
services服务定义,核心每个服务一个镜像或 build
volumes命名卷声明数据库、上传目录
networks自定义网络前后端隔离
x-*扩展字段(锚点复用)统一复制日志配置

一个完整示例:web + mysql + redis + nginx

name: tools-site
services:
  nginx:
    image: nginx:1.27-alpine
    ports: ["80:80", "443:443"]
    volumes: ["./nginx/conf.d:/etc/nginx/conf.d:ro", "./nginx/certs:/etc/nginx/certs:ro"]
    depends_on:
      web: { condition: service_healthy }
    networks: [frontend]
    restart: unless-stopped
  web:
    build: { context: .., dockerfile: deploy/Dockerfile }
    image: myapi:1.4.0
    environment:
      TZ: Asia/Shanghai
      DATABASE_URL: mysql://app:${MYSQL_PASSWORD}@mysql:3306/appdb
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
    volumes: ["uploads:/app/uploads"]
    depends_on:
      mysql: { condition: service_healthy }
      redis: { condition: service_started }
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8000/healthz"]
      interval: 30s
      retries: 3
      start_period: 20s
    networks: [frontend, backend]
    restart: unless-stopped
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: appdb
      MYSQL_USER: app
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      TZ: Asia/Shanghai
    volumes: ["mysqldata:/var/lib/mysql"]
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      retries: 10
      start_period: 40s
    networks: [backend]
    restart: unless-stopped
  redis:
    image: redis:7.4-alpine
    command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
    volumes: ["redisdata:/data"]
    networks: [backend]
    restart: unless-stopped
volumes:
  mysqldata:
  redisdata:
  uploads:
networks:
  frontend:
  backend:
    internal: true

depends_on 与健康检查配合

depends_on 默认只保证“启动顺序”,不保证“依赖已经可用”——MySQL 容器起来后还要几十秒才接受连接。

写法含义何时用
depends_on: [db]只按顺序启动依赖启动即可用(如静态文件服务)
condition: service_started容器已启动同上
condition: service_healthy健康检查通过数据库、消息队列等需要预热
condition: service_completed_successfully一次性任务成功退出数据库迁移

应用自己也要带重试连接逻辑,不能指望一次连上。

环境变量与 .env

# .env —— 与 docker-compose.yml 同目录,Compose 自动读取用于「变量替换」
# 它不会自动注入容器,注入容器要靠 environment 或 env_file
MYSQL_ROOT_PASSWORD=change-me-root
MYSQL_PASSWORD=change-me-app
REDIS_PASSWORD=change-me-redis
COMPOSE_PROJECT_NAME=tools-site

.env 必须写进 .gitignore;团队共享的是 .env.example,里面只放变量名和说明,不放真值。

常用命令与验证

docker compose config -q && echo compose-ok   # 校验语法与变量替换,不启动任何东西
docker compose up -d --build                  # 代码变了必须带 --build
docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'
docker compose logs -f --tail 100 web
docker compose exec web sh -c 'nc -vz mysql 3306 && nc -vz redis 6379'
docker compose port mysql 3306 || echo 'mysql 未映射到宿主机,符合预期'
docker compose restart web
docker compose down                           # 保留卷;down -v 会连数据一起删

docker compose ps 的健康列显示 healthync -vz mysql 3306 返回 succeeded,说明编排与依赖顺序都对了;docker volume ls 能看到 tools-site_mysqldata,说明数据落在命名卷上。

常见坑

现象做法
改了代码没加 --build服务还是旧版本,查半天up -d --build
depends_on 不配健康检查应用启动时数据库未就绪,连接被拒healthcheck + service_healthy
.env 提交进 Git密码泄露.gitignore 排除,只提交 .env.example
docker compose run 排查起了新容器,看到的不是线上环境排查一律用 exec
数据库没挂卷重建容器数据全丢mysqldata:/var/lib/mysql 必须写
所有服务放同一网络数据库可被前端容器直连,攻击面变大frontend/backend 两个网络隔离

小结:Compose 用一份 YAML 描述整套服务,v2 的命令是 docker compose(带空格),要点是四个——用 healthcheckcondition: service_healthy 管启动顺序、用 .env 做变量替换且不入库、用命名卷保证数据、用多网络把数据库与公网入口隔开;改完代码记得 up -d --build,改完配置先 docker compose config 校验。

笔记加载中…