Docker Compose 多服务编排实战:.env、健康检查、依赖顺序与一键迁移(2026)

Docker Compose 的核心价值是用一份声明式 YAML 描述「一组服务及其依赖关系」,让 docker run 里那些容易抄错的长参数、网络、卷挂载全部沉淀进版本可控的文件里。 关键就三条:用 .env 把密码和端口从 YAML 里抽出来;用 healthcheck + depends_on: condition: service_healthy 解决「数据库还没起来应用就崩」的顺序问题;用命名卷持久化数据,迁移时连同卷一起搬走。我用一份「PostgreSQL + Umami 网站统计」的 compose 文件走完整流程,从 up -d 到应用可访问、再到备份迁移,全程可复现。

上面这段就是全文的结论块,可以直接拿走。下面是完整模板、逐步命令和排错。

Docker Compose 的服务编排结构

什么时候该从 docker run 升级到 Compose?

结论:只要「两个以上服务需要互访」,或者「同一个服务你要反复重建」,就该上 Compose。

场景 推荐方式 原因
单个无状态容器 docker run 一条命令够了
应用 + 数据库 + 缓存 Compose 服务名即主机名,自动建内部网络
需要固定启动顺序 Compose depends_on + healthcheck 可声明
要在多台机器复刻同一套 Compose 一份 YAML + 一份 .env 即可迁移
多节点调度 / 自愈 K8s / Swarm 单机 Compose 不做集群调度

一句话:单机、多服务、要可复现,就是 Compose 的甜点区——它是「单机 Docker 的手脚」,不是「K8s 的替代」。

配置步骤速览

Compose 编排五步

第一步:把秘密抽进 .env

步骤 1:写 .env

$ mkdir -p /opt/umami && cd /opt/umami
$ umask 077 && cat > .env <<'EOF'
DB_PASSWORD=换成你自己的强密码
APP_SECRET=换成你自己的随机串
EOF

.env 和 compose.yml 同目录时,Compose 会自动加载,YAML 里写 ${DB_PASSWORD} 就能引用。把密码留在 YAML 里提交到 Git 是最常见的泄密方式,.env 要写进 .gitignore。

第二步:写 compose.yml

步骤 2:写 compose.yml

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    networks: [backend]

  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://umami:${DB_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
    ports:
      - "127.0.0.1:3000:3000"
    networks: [backend]

volumes:
  db-data:

networks:
  backend:

三个要点:服务名即主机名(db 就能被 umami 直接解析到,因为同在 backend 网络里);端口绑 127.0.0.1(见后文踩坑 2);卷用命名卷 db-data,而不是相对路径。

第三步:加健康检查与依赖顺序

步骤 3:healthcheck 与 depends_on

  db:
    # ……
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
      interval: 10s
      timeout: 5s
      retries: 5

  umami:
    # ……
    depends_on:
      db:
        condition: service_healthy

这是全篇最关键的两行。 没有 healthcheck 时,depends_on 只保证「db 容器已创建」,不等「PostgreSQL 可以接受连接」——于是应用启动第一秒就报 connection refused 然后崩溃重启,你在日志里看到的是一个假故障。

第四步:校验、启动、观察

步骤 4:启动与查看

$ docker compose config          # 先校验语法与变量替换,不启动
$ docker compose up -d           # 后台启动全部服务
$ docker compose ps              # 看状态与健康度
$ docker compose logs -f umami   # 跟一个服务的日志

docker compose config 会把你 YAML 里 ${...} 已替换、默认值已展开的最终效果打印出来——启动前一定先跑这一条,能把「变量没生效」「缩进错了」这类问题在造成事故前抓出来。

第五步:备份卷与迁移

步骤 5:备份命名卷与迁移

数据在命名卷里(默认落在 /var/lib/docker/volumes/<项目名>_db-data),迁移或备份要单独打包:

# 把命名卷打包成一个 tar 到当前目录
$ docker run --rm \
    -v umami_db-data:/data \
    -v "$PWD":/backup alpine \
    tar czf /backup/db-$(date +%F).tgz -C /data .

# 迁到新机器:复制 compose.yml + .env + 这个 tar,还原后再 up
$ docker run --rm \
    -v umami_db-data:/data \
    -v "$PWD":/backup alpine \
    tar xzf /backup/db-2026-10-04.tgz -C /data
$ docker compose up -d

只复制 YAML 不复制卷,新机器起来的是一个空数据库——这是「迁移后数据没了」的头号原因。

多环境怎么切换?用 override 文件

结论:Compose 原生支持「基础文件 + 覆盖文件」的叠加,不用为开发和生产各维护一份完整 YAML。 它默认读取 compose.yml,若同目录还存在 compose.override.yml,会自动再叠加上去;部署到生产时用 -f 显式指定文件即可。

# 开发:自动叠加 compose.override.yml(暴露端口、挂源码目录)
$ docker compose up -d

# 生产:只用基础文件,外加一份 prod 覆盖(固定镜像 tag、收回端口)
$ docker compose -f compose.yml -f compose.prod.yml up -d

compose.prod.yml 里只写和基础文件不同的部分:把镜像从 :latest 换成固定版本号(避免某天自动更新把服务更挂)、把端口收回到 127.0.0.1、加上日志上限。多个 -f 按顺序叠加,后面的覆盖前面的同名键——这条规则让你能用最小的 diff 表达环境差异。

开发态可以把源码目录直接挂进容器(volumes: ["./src:/app"])改代码即生效;生产态绝不挂源码,只靠构建好的镜像。开发和生产共用同一份 compose.yml、差异全在覆盖文件里,两边行为才真正一致。

四个真会踩的坑

  1. depends_on 不等于「等就绪」。 如上,必须配 healthcheck + condition: service_healthy。数据库、Redis、消息队列这类有启动时间的服务,全都要这么做。
  2. 端口写 "3000:3000" 等于对公网敞开。 冒号左边不写 127.0.0.1: 时,Docker 会绑 0.0.0.0,并且直接往 iptables 插规则、绕过 UFW——这正是 UFW 挡不住 Docker 端口 的成因。内部服务统一写 "127.0.0.1:3000:3000",公网入口交给反代或 Tunnel。
  3. .env 里的 $ 会被 Compose 二次插值。 密码里带 $ 时,Compose 会把它当变量引用,导致密码被改掉、连接失败。含 $ 的密码要写成 $$(转义成一个字面量 $),或用 env_file 指向一个不走插值的文件。
  4. version: 顶部字段已废弃。 新版 Compose 会提示 the attribute version is obsolete。删掉它,只留 services / volumes / networks 顶层键即可,不要再照抄老教程。

Docker Compose 常见问题(2026)

Q:docker compose down 会删掉我的数据吗?
A:默认不会。down 只删容器和网络,命名卷保留。加了 -v 才会连卷一起删(docker compose down -v)——这条命令在数据库栈上等于删库,执行前务必确认卷已备份。

Q:depends_on 写了为什么应用还是连不上数据库?
A:因为你没配健康检查。默认 depends_on 只保证容器已创建,不保证服务已就绪。加上 healthcheck 并把 depends_on 写成 condition: service_healthy 即可。

Q:怎么把一套 Compose 栈迁到新机器?
A:四样东西:compose.yml、.env、镜像(新机器 docker compose pull 或直接 up 会自动拉)、命名卷的数据。前两者复制过去,卷用 tar 打包还原。少了卷数据,数据库就是空的。

Q:Compose 和 Dockerfile 是什么关系?
A:不冲突,是两层。Dockerfile 定义「一个镜像怎么构建」,Compose 定义「多个镜像怎么协作运行」。Compose 里既可以用现成镜像,也可以用 build: . 指向一个 Dockerfile 现构建。

Q:一个主机上能跑多套 Compose 栈吗?会不会冲突?
A:可以,靠项目名隔离(默认取目录名)。不同目录下的 compose.yml 各是独立项目,容器名、网络、卷都会带项目前缀。要显式指定用 -p 项目名。

Q:容器日志会把磁盘占满吗?
A:会。Docker 默认的 json-file 日志驱动没有单文件大小上限,一个高频输出的容器几天就能写掉几 GB,而 du 常常看不出来。给服务加 logging 段限制大小(max-size: 20m + max-file: 3),根治思路与排查命令见 VPS 磁盘满了怎么排查。

一句总结

Compose 把「一坨 docker run」变成了「一份可版本化、可复现的声明」。真正决定它好不好用的,是那几个容易被老教程带偏的细节:.env 分离秘密、healthcheck 管顺序、127.0.0.1 控暴露面、命名卷管数据。把这四点做对,迁移和排障都会从「玄学」变成「照抄文件」。

相关阅读

上一篇 Cloudflare Zero Trust Access 实战:给自建服务加一道免费登录门(2026)
下一篇 Linux 服务器疑似被入侵怎么排查?挖矿进程、异常 cron、SSH 后门四步定位(2026)