Docker Compose怎么使用?多容器应用部署实战教程

一个稍微正经点的应用,很少只有一个容器。典型的 Web 系统至少要有应用服务、数据库、缓存三件套,可能还有反向代理、消息队列。如果全靠 docker run 一条条敲,你会陷入几个麻烦:容器之间的网络要手工建、依赖顺序要手工控、环境变量和端口散落在命令历史里、换台机器部署时根本记不住当初敲了什么。Docker Compose 就是为了解决这个"多容器编排"的痛点而生的。它用一个 YAML 文件描述整套应用,一条命令启动全部、一条命令拆掉全部。这篇文章讲清 Compose 的 YAML 结构、常用命令、变量与依赖管理,并给出一套可以直接跑的"Web + MySQL + Redis"完整示例,最后对比它和 docker run 的差异。

一、Compose 是什么,解决什么问题

Compose 的核心思想是"声明式"。你不再描述"先执行 A、再执行 B"的过程,而是在一个 compose.yaml 文件里声明"我要这么一套服务,它们长这样、这样连、这样存数据"。Compose 读了这个文件,替你算出该创建哪些容器、网络、卷,然后一次性拉起来。

它主要解决四件事:把一组容器当成一个整体来管理(启动、停止、重建);自动创建一个专用网络,让服务之间用服务名互相访问,不用记 IP;用文件固化端口、环境变量、卷挂载、依赖关系,使配置可版本化、可复现;通过 up/down 一条命令完成整套环境的起停,特别适合本地开发和中小型部署。

需要说明版本差异:老版本用的是独立的 docker-compose 二进制(v1,Python 写的),新版本是 docker compose 子命令(v2,Go 写的,随 Docker 一起装)。现在统一推荐用 docker compose。本文示例用 v2 语法。

二、compose.yaml 的顶层结构

一个 Compose 文件通常包含四个顶层字段:

字段 作用 是否必需
services 定义每个容器(镜像、端口、环境变量、卷、依赖等) 必需
networks 定义自定义网络 可选,不写会自动建默认网络
volumes 定义命名卷 可选,用了命名卷才需要
configs / secrets 注入配置文件与敏感信息 可选,进阶用法

下面的骨架展示了各字段的位置关系:

services:
  web:
    image: nginx:1.25-alpine
    ports:
      - "8080:80"
    networks:
      - app-net
    volumes:
      - web-data:/usr/share/nginx/html

volumes:
  web-data:

networks:
  app-net:
    driver: bridge

注意 YAML 的缩进:用空格,不用 Tab。绝大多数 Compose 报错都是缩进或键名拼错导致的,写完可以用 docker compose config 校验并展开。

三、常用命令速查

Compose 的命令不多,掌握这几条基本够用:

# 启动全部服务(后台)  -d 表示 detached
docker compose up -d

# 前台启动并跟踪日志(调试常用,Ctrl+C 停止)
docker compose up

# 只启动指定服务及其依赖
docker compose up -d web

# 构建(有 build: 字段的服务)并启动
docker compose up -d --build

# 查看服务状态
docker compose ps

# 查看日志(-f 跟随,--tail 只看最后 N 行)
docker compose logs -f --tail=100 web

# 进入运行中的容器执行命令
docker compose exec web sh
docker compose exec db mysql -uroot -p

# 停止并删除容器、网络(默认保留命名卷)
docker compose down

# 连卷一起删(危险:会删除数据卷里的数据)
docker compose down -v

# 重启单个服务
docker compose restart web

两个容易混淆的点:docker compose down 默认不会删命名卷,这是为了防止误删数据,要连数据一起清掉必须显式加 -v;docker compose exec 是进正在运行的容器,docker compose run 是另起一个新容器执行一次性命令,别搞混。

四、变量与 .env 文件

硬编码在 YAML 里的密码、端口、版本号很快就会失控,Compose 支持用变量。有两种来源:一是同目录下的 .env 文件(自动加载),二是 shell 环境变量。

# .env 文件(与 compose.yaml 同目录,不要提交到代码仓库)
MYSQL_ROOT_PASSWORD=change_this_root_pwd
MYSQL_DATABASE=appdb
MYSQL_USER=appuser
MYSQL_PASSWORD=change_this_app_pwd
WEB_PORT=8080
IMAGE_TAG=1.25-alpine

在 YAML 里用 ${变量名} 引用,还可以给默认值 ${VAR:-默认值}:

services:
  web:
    image: nginx:${IMAGE_TAG:-alpine}
    ports:
      - "${WEB_PORT:-8080}:80"

这里有个安全提醒:.env 里往往包含密码,必须加入 .gitignore,绝不要把它提交到代码仓库。生产环境建议改用 secrets 或由部署系统注入环境变量。

五、depends_on 与 healthcheck

depends_on 控制启动顺序,但要特别注意它只保证"容器被启动了",不保证"服务已经能用了"。数据库容器启动到能接受连接有几秒到几十秒的差距,如果应用在数据库没就绪时就连接,很可能直接崩溃。

services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy   # 等 db 健康后再启动
      cache:
        condition: service_started

要让 condition: service_healthy 生效,被依赖的服务必须定义 healthcheck:

services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

注意 healthcheck 里要写 $$MYSQL_ROOT_PASSWORD(两个美元符号),这样 Compose 会把 $$ 转义成一个 $ 交给容器内的 shell 展开,而不是在 Compose 解析阶段就被替换掉。start_period 给容器一段"养成期",这期间健康检查失败不计入 retries,对数据库这类启动慢的服务很有用。

六、完整示例:Web + MySQL + Redis 三件套

下面这份 compose.yaml 是一个可以直接跑的最小生产级示例,包含应用(以官方 nginx 示例替代)、MySQL、Redis,演示了端口、卷、网络、环境变量、健康检查、初始化脚本等关键点。

services:
  web:
    image: nginx:${IMAGE_TAG:-1.25-alpine}
    container_name: demo-web
    restart: unless-stopped
    ports:
      - "${WEB_PORT:-8080}:80"
    networks:
      - app-net
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: mysql:8.0
    container_name: demo-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      TZ: Asia/Shanghai
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"          # 生产环境建议不对外暴露,仅内网访问
    volumes:
      - db-data:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 40s
    networks:
      - app-net

  cache:
    image: redis:7-alpine
    container_name: demo-cache
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
    networks:
      - app-net

volumes:
  db-data:
  redis-data:

networks:
  app-net:
    driver: bridge

几个关键点说明。第一,restart: unless-stopped 让容器在宿主机重启或异常退出后自动拉起,生产环境基本必备。第二,MySQL 的 db-data 命名卷把数据持久化到容器之外,删容器不丢库。第三,./initdb 挂到 /docker-entrypoint-initdb.d,MySQL 官方镜像只在数据目录为空时执行其中脚本,用来做首次初始化。第四,command 覆盖默认启动参数,此处把字符集设成 utf8mb4,避免中文乱码。第五,服务之间通过服务名互访——应用里数据库地址写 db、缓存地址写 cache,Compose 的内置 DNS 会解析到对应容器。

启动与验证:

# 目录结构:compose.yaml / .env / nginx/conf.d/ / html/ / initdb/
docker compose up -d
docker compose ps
docker compose logs -f --tail=50
# 从 web 容器里测试到 db 的连通性(服务名即主机名)
docker compose exec web sh -c "ping -c1 db || getent hosts db"
docker compose down        # 停止,保留数据卷

七、Compose 与 docker run 的对比

理解 Compose 的价值,最好拿它和 docker run 比。

维度 docker run Docker Compose
配置形式 命令行参数,命令历史里 声明式 YAML 文件,可版本化
多容器管理 逐个手工起,顺序靠自己 一条命令起停整套
服务发现 需手工建网络并记录 IP 自动建网络,服务名即域名
依赖顺序 手工 sleep 或用脚本控制 depends_on + healthcheck
可复现性 差,依赖操作记忆 强,文件即部署说明
环境差异 开发生产易不一致 同文件跨环境部署
适用规模 单容器、临时调试 多容器、中小型部署、开发环境

一个等价对照能说明问题。用 docker run 部署 MySQL 大概是:

docker network create app-net
docker run -d --name db --network app-net --restart unless-stopped \
  -e MYSQL_ROOT_PASSWORD=xxx -e MYSQL_DATABASE=appdb \
  -v db-data:/var/lib/mysql -v ./initdb:/docker-entrypoint-initdb.d:ro \
  mysql:8.0

同样的事情,Compose 就是上面的 YAML 里一段 db: 定义。容器越多,docker run 的维护成本越呈指数上升,而 Compose 基本是线性增加几行。所以结论很直接:单容器调试用 docker run,多容器(两个及以上)就上 Compose。

需要提醒的是,Compose 是"单机编排"工具,它管理的是同一台宿主机上的容器。跨多台机器的集群编排是 Kubernetes 的领域。中小企业多数场景用一台(或几台各跑一套)Compose 就够了,不必为了用它而引入一整套 K8s。

常见问题

Q:改了 compose.yaml 后怎么让改动生效?
改配置后执行 docker compose up -d,Compose 会以最小改动重建受影响的容器。如果是改了 build: 相关的 Dockerfile,加 --build。

Q:docker compose down 会不会删数据?
默认不会,命名卷会保留。只有加 -v 才会连卷一起删。生产上养成"默认 down、确认后再加 -v"的习惯。

Q:服务之间为什么能直接用名字访问?
Compose 会创建一个默认网络并把所有服务接进去,同时提供内置 DNS,把服务名解析到对应容器 IP。所以连接串里写 db、cache 即可,无需知道具体 IP。

Q:depends_on 能保证数据库先就绪吗?
不能,它只管启动顺序。要保证"就绪",被依赖服务必须有 healthcheck,并配合 condition: service_healthy。否则应用仍需自己做连接重试。

Q:compose.yaml 和 docker-compose.yml 有什么区别?
就是文件名不同。Compose v2 推荐用 compose.yaml(或 compose.yml),旧文件 docker-compose.yml 仍被识别。建议统一用新名。

Q:Compose 能不能用于生产环境?
可以,很多中小型业务就是在一台配置较好的云主机上用 Compose 跑整套服务,配合 restart: unless-stopped 和日志限额即可稳定运行。它的局限是单机,没有跨机调度和高可用;当单机性能或可用性成为瓶颈时,再考虑上 Kubernetes。

Q:容器里的数据怎么备份?
Compose 管理的命名卷同样需要备份。常见做法是用一个临时容器挂载该卷并打包成 tar,具体命令在数据卷那一篇会详细展开。原则是:卷里的数据要定期导出到容器之外、机器之外的存储。

总结

Compose 的本质是把"一套多容器应用"声明在一个 YAML 里,实现整体起停、自动组网、配置固化。核心字段是 services,按需配 networks 和 volumes。

常用命令集中在 up -d、down、ps、logs、exec、build;牢记 down 默认不删卷,down -v 会删数据,执行前务必确认。

变量用 .env 管理,注意把 .env 排除出版本控制;依赖管理用 depends_on 配合 healthcheck,靠健康检查而非单纯的启动顺序来保证就绪。

真实部署里,restart: unless-stopped、数据卷持久化、初始化脚本挂载、字符集参数这几项几乎是标配,示例中的三件套可以按需增删服务直接复用。

选型上,单容器调试用 docker run,两容器以上就用 Compose;跨多机集群才轮到 Kubernetes,中小企业不必过度设计。