跳到正文
Docker Compose:为什么我不手敲一串 docker run
Docker Compose:为什么我不手敲一串 docker run

Docker Compose:为什么我不手敲一串 docker run

真实项目很少只有一个容器。这篇不讲 Compose 语法大全,讲我用手敲 docker run 翻过什么车、为什么把整套服务写进一个 yml,以及那条差点把数据删没了的教训。

速读#

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,或干脆把数据库放到托管服务;重点不是「必须命名卷」,而是生命周期不能和容器绑死。我曾经把数据只写在容器层,删容器时一起删掉,这个教训足够让我每次上线前都检查挂载。

Terminal window
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 语法表,是三件事:

  1. 环境该是代码
  2. 有状态数据必须脱离容器层,卷仍然不是备份
  3. 启动顺序 ≠ 就绪

三者一起,才支撑「换机器一键复现,且数据不丢」。

版权许可

CC BY-NC-SA 4.0 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

相关文章

s1oopX

登录 s1oopX