速读#
Compose 解决的不是少敲几条命令,而是把整套服务环境写成可提交、可复现、可 diff 的文件。这篇真正要记住三件事:环境该是代码,有状态数据必须离开容器可写层,启动顺序不等于服务就绪。
手敲的问题:下次很难和上次一样#
拿这个博客说:WordPress、数据库、Redis 三个服务。一个个 docker run,命令又长又容易错——漏环境变量、端口写反,排半天。
更早做 Django + Redis + Celery,光本地环境搭起来就半小时。手敲最大的问题不是慢,是下次重建,大概率和上次不一样。
环境该是代码#
Compose 把整套服务写进一个 compose.yml。选择它的理由就一句:环境该是代码——可提交、可复现、可 diff。
这三个「可」各自值钱:可提交,环境变动有记录;可复现,换机器可以用同一份声明重新拉起;可 diff,提交前能看清哪一行变了。这里的「复现」是配置和拓扑可复现,不自动等于字节级一致:浮动镜像标签会变化,构建上下文、基础镜像和外部数据也可能不同。真要严格复现,还得提交 Dockerfile 与锁文件,并按需要固定镜像 digest。docker compose up -d 会让实际状态向声明收敛,配置变化时重建对应服务,但应用自己的初始化脚本仍要设计成可重复执行。
services: app: build: . depends_on: [db] environment: DB_HOST: db DB_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD} db: image: mysql:8.4 environment: MYSQL_DATABASE: app MYSQL_USER: app MYSQL_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD} MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?set DB_ROOT_PASSWORD} volumes: - db_data:/var/lib/mysql
volumes: db_data:密码来自未提交的 .env 或部署平台 secret,仓库只放 .env.example 的变量名。${VAR:?message} 会在变量缺失时直接失败,避免 MySQL 起到一半才发现配置空了;提交前我先跑 docker compose config -q 做解析校验,不把会展开 secret 的完整 docker compose config 输出贴进日志。
差点删没数据:破坏性藏在 -v 里#
有状态数据必须放在容器可写层之外。 单机 Compose 最省事的选择是命名卷:容器删除或重建时卷默认仍在。也可以使用明确管理的 bind mount,或干脆把数据库放到托管服务;重点不是「必须命名卷」,而是生命周期不能和容器绑死。我曾经把数据只写在容器层,删容器时一起删掉,这个教训足够让我每次上线前都检查挂载。
docker compose down # 默认保留命名卷docker compose down -v # 连数据卷一起删| 命令 | 数据卷 |
|---|---|
down | 保留 |
down -v | 删除该 Compose 项目声明的命名卷和附着的匿名卷;external 卷除外 |
看上去只是「停服务」,加个 -v 就把项目数据带走。破坏性命令绝不盲执行:先用 docker compose ls 确认项目,用 docker compose config --volumes 确认声明的卷,再检查备份能否恢复。只看 docker volume ls 不够,它不能告诉我这个卷里的业务数据是否还有唯一副本。
命名卷解决的是「重建容器不顺手删数据」,不解决宿主机磁盘损坏、账号误删、数据逻辑损坏和供应商故障。数据库仍要做一致性备份并定期恢复演练;卷不是备份。
服务名就是主机名#
DB_HOST: db——app 用服务名 db 连库。Compose 起服务时会自动建一个内部网络,并带内部 DNS 把服务名解析成容器地址;不用手动建网络,更不用查容器 IP——容器重建后 IP 会变,服务名不会。
这个机制的适用范围比想象大:后来我把 cloudflared 和应用放进同一个 Compose 网络,隧道配置里直接写 http://app:8080,访问的是应用的容器端口,不需要把 8080 发布到宿主机。同一 Compose 网络里,服务名可以作为 DNS 名;跨 Compose 项目则要显式加入同一个 external 网络,不能假设彼此自动可见。
depends_on 只管启动顺序#
depends_on 让 db 先起、app 后起。但要清醒:
进程起了 ≠ 依赖就绪。 db 容器起来了,不代表 MySQL 已经能接连接。app 可能在库还没 ready 时去连,直接失败。
| 概念 | 含义 |
|---|---|
| 启动顺序 | 容器谁先 start |
| 就绪 | 进程内服务是否真能接请求 |
要让 depends_on 真等到「能接连接」,得给依赖配 healthcheck,再声明等它健康:
db: image: mysql:8.4 healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"] interval: 5s timeout: 3s retries: 20 start_period: 20s
app: depends_on: db: condition: service_healthy$$ 是把美元符号留给容器内 shell,避免 Compose 在宿主机提前展开变量。即使配了 service_healthy,应用侧仍必须带有上限和退避的连接重试:这个条件只影响 Compose 本次启动顺序,数据库之后重启或变为 unhealthy 时,Compose 不会自动替应用重建连接。两道兜底不是重复,而是各管一段时间窗。
这篇留下的不是 Compose 语法表,是三件事:
- 环境该是代码
- 有状态数据必须脱离容器层,卷仍然不是备份
- 启动顺序 ≠ 就绪
三者一起,才支撑「换机器一键复现,且数据不丢」。
