外观
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: secretbash
docker compose up -d
docker compose pstext
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.limits | Compose 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.2bash
docker compose config --quiet && echo ok
docker compose config | head -30text
okdocker 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(或直接不等)。
健康检查怎么写才靠谱:
| 服务 | 检查命令 |
|---|---|
| MySQL | mysqladmin ping -h 127.0.0.1 -uroot -p$PASS |
| PostgreSQL | pg_isready -U postgres |
| Redis | redis-cli -a $PASS ping |
| Kafka | kafka-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 -dcompose.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 | 见 资源与日志 |