Docker Compose 编排多服务
一个稍具规模的应用至少有三个进程:应用、数据库、缓存,再加一个 Nginx 就是四个。手工 docker run 四次,参数会越来越长,容器之间的启动顺序还得靠人盯。Compose 用一个 YAML 文件描述整套服务,up -d 一次拉起,这才是单机部署的正确姿势。
Compose v1 与 v2 的区别
| 维度 | docker-compose v1 | docker compose v2 |
|---|---|---|
| 形态 | 独立的 Python 程序 | Docker CLI 插件 |
| 命令 | docker-compose up -d | docker 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 的健康列显示 healthy、nc -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(带空格),要点是四个——用 healthcheck 加 condition: service_healthy 管启动顺序、用 .env 做变量替换且不入库、用命名卷保证数据、用多网络把数据库与公网入口隔开;改完代码记得 up -d --build,改完配置先 docker compose config 校验。