如果你刚接触 Docker,可能会觉得一个 compose.yaml 就能搞定所有事情。但随着项目从本地开发走向生产部署,你会发现:开发时需要热重载、调试工具,生产时却需要资源限制、自动重启。把这些配置全塞进一个文件,不仅混乱,还容易把开发专用功能误带到生产环境。Docker 官方推荐的多文件分层策略,正是解决这个问题的标准方法。本文专为有一点 Docker 基础的初学者设计,手把手带你从零搭建一个可实操的项目,彻底搞懂多文件、profiles 和环境变量的配合使用。

本文将带你完成一个 PHP + MariaDB + phpMyAdmin 的示例项目,并逐步将其改造成支持开发和生产两套环境的配置。你会学到:

  • 如何用 compose.yaml + compose.override.yaml + compose.prod.yaml 拆分配置。

  • Docker Compose 的自动加载规则,以及 -f 参数的影响。

  • 如何用 profiles 控制可选服务(如调试工具)的启停。

  • 如何用 .env 文件管理环境变量。

  • 多文件合并的核心规则与常见陷阱。

  • 最终形成一套“多文件 + profiles + 环境变量”的最佳实践。

所有代码都可以直接复制运行,跟着做一遍就能掌握。

一、准备工作

  1. 开始之前,请务必安装并启动 Docker Desktop。

  2. 创建一个项目目录:

mkdir compose-demo && cd compose-demo

二、初始版本:一个简单的 compose.yaml

我们先写一个最基础的 compose.yaml,包含三个服务:PHP 应用(server)、MariaDB 数据库(db)和 phpMyAdmin。

services:
  server:
    build:
      context: .
    ports:
      - "9000:80"
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root
    volumes:
      - ./src:/var/www/html
    develop:
      watch:
        - action: sync
          path: ./src
          target: /var/www/html

  db:
    image: mariadb
    restart: always
    environment:
      - MARIADB_ROOT_PASSWORD=example
      - MARIADB_DATABASE=example
    volumes:
      # db-data 属于命名卷,而非路径。如果误写为 ./db-data:/var/lib/mysql,Docker 会将其识别为绑定挂载,在宿主机产生 db-data 目录。
      # 如果改成绑定挂载,顶层的 volumes: db-data: 声明就不再需要了,应该删除,否则 docker compose up 时仍会创建一个无用的命名卷。
      - db-data:/var/lib/mysql 
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  phpmyadmin:
    image: phpmyadmin
    ports:
      - "9090:80"
    depends_on:
      - db
    environment:
      - PMA_HOST=db

volumes: 
  db-data:

同时创建一个简单的 Dockerfile:

FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_mysql
COPY ./src /var/www/html/

再创建 src/index.php:

<?php
    echo "Hello Docker!";

现在运行:

docker compose up -d --build

访问 http://localhost:9000 应该能看到 “Hello Docker!”。这个版本能跑,但所有环境共用同一套配置,无法区分开发和生产。

三、为什么需要环境隔离?

开发时我们想要:

  • 修改代码立即生效(热重载)。

  • 安装开发依赖(如 PHPUnit)。

  • 启动调试工具(如 phpMyAdmin)。

生产时我们想要:

  • 代码打包进镜像,不挂载宿主机目录。

  • 不安装开发依赖。

  • 限制 CPU 和内存。

  • 异常自动重启。

  • 默认不启动调试工具。

如果把这些都写进一个文件,就需要大量注释和手动切换,容易出错。多文件策略就是为解决这个问题而生。

四、方案一:多文件覆盖(官方推荐)

Docker Compose 允许你指定多个文件,后面的文件会覆盖前面的。我们拆成三个文件:

  • compose.yaml:基础配置,所有环境共用。

  • compose.override.yaml:开发专用,自动加载。

  • compose.prod.yaml:生产专用,手动指定。

1. 基础文件 compose.yaml

只保留公共部分,把开发专用的 develop 和挂载去掉:

services:
  server:
    build:
      context: .
    ports:
      - "9000:80"
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root

  db:
    image: mariadb
    restart: always
    environment:
      - MARIADB_ROOT_PASSWORD=example
      - MARIADB_DATABASE=example
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  phpmyadmin:
    image: phpmyadmin
    ports:
      - "9090:80"
    depends_on:
      - db
    environment:
      - PMA_HOST=db

volumes:
  db-data:
2. 开发覆盖 compose.override.yaml

这个文件会被自动加载,所以开发时直接 docker compose up 即可。

services:
  server:
    build:
      args:
        - COMPOSER_NO_DEV=0
    volumes:
      - ./src:/var/www/html
    develop:
      watch:
        - action: sync
          path: ./src
          target: /var/www/html

同时修改 Dockerfile 支持构建参数:

FROM composer:lts AS deps
ARG COMPOSER_NO_DEV=1
WORKDIR /app
COPY composer.json composer.lock ./
RUN if [ "$COMPOSER_NO_DEV" = "1" ]; then \
      composer install --no-dev --no-interaction; \
    else \
      composer install --no-interaction; \
    fi

FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_mysql
COPY --from=deps /app/vendor /var/www/html/vendor
COPY ./src /var/www/html/

COMPOSER_NO_DEV 是自定义参数,你可以改成任何名字,只要 Dockerfile 和 compose 文件对应即可。

在 Dockerfile 同级目录添加 compose.json 与 compose.lock,文件内容请参考:https://github.com/docker/docker-php-sample/。

3. 生产覆盖 compose.prod.yaml

生产环境需要显式指定这个文件。

services:
  server:
    build:
      args:
        - COMPOSER_NO_DEV=1
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
  db:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
  phpmyadmin:
    profiles: ["tools"]
4. 运行方式
# 开发:override 自动加载
docker compose up -d --build

# 开发热重载
docker compose watch

# 生产:必须显式指定 prod 文件
docker compose -f compose.yaml -f compose.prod.yaml up -d --build

# 生产需要 phpMyAdmin 时
docker compose -f compose.yaml -f compose.prod.yaml --profile tools up -d
5. 自动加载规则(重要!)
  • 当你执行 docker compose up 且 没有指定 -f 时,Compose 会按顺序查找:

  1. compose.yaml(或 compose.yml)——必须存在;

  2. compose.override.yaml(或 compose.override.yml)——可选,存在则自动合并。

  • 一旦你指定了 -f,自动加载就完全失效。例如:

docker compose -f compose.yaml up
# 此时 compose.override.yaml 不会被加载!

所以生产环境必须写成:

docker compose -f compose.yaml -f compose.prod.yaml up -d

五、方案二:profiles 开关可选服务

如果你不想为可选服务单独建文件,可以用 profiles。在 compose.yaml 中给服务加上 profiles:

services:
  phpmyadmin:
    image: phpmyadmin
    profiles: ["dev", "tools"]   # 只在指定 profile 时启动

然后:

# 开发时启动 dev profile
docker compose --profile dev up -d

# 生产时如果需要工具
docker compose --profile tools up -d

没有指定 profile 的服务会始终启动。profiles 适合控制调试工具、管理后台等可选服务。

六、方案三:环境变量 + .env 文件

用 ${VAR:-默认值} 在 compose 文件中引用变量,不同环境使用不同的 .env 文件。

services:
  server:
    build:
      context: .
      args:
        COMPOSER_NO_DEV: ${COMPOSER_NO_DEV:-1}
    restart: ${RESTART_POLICY:-no}
    ports:
      - "${SERVER_PORT:-9000}:80"
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DB_HOST=db
      - DB_NAME=example
      - DB_USER=root

  db:
    image: mariadb
    restart: always
    environment:
      - MARIADB_ROOT_PASSWORD=example
      - MARIADB_DATABASE=example
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  phpmyadmin:
    image: phpmyadmin
    ports:
      - "9090:80"
    depends_on:
      - db
    environment:
      - PMA_HOST=db

volumes:
  db-data:

创建 .env.dev:

COMPOSER_NO_DEV=0
RESTART_POLICY=no
SERVER_PORT=9000
SERVER_MEMORY_LIMIT=

创建 .env.prod:

COMPOSER_NO_DEV=1
RESTART_POLICY=always
SERVER_PORT=80
SERVER_MEMORY_LIMIT=256M

运行:

docker compose --env-file .env.dev up -d
docker compose --env-file .env.prod up -d

环境变量适合处理简单的值差异,但复杂的结构性差异(比如开发有 develop 块,生产没有)还是多文件更清晰。

七、最佳实践:多文件 + profiles + 环境变量组合

把上面三种方案结合起来,就是现代推荐的做法。项目结构:

compose-demo/
├── compose.yaml            # 基础定义
├── compose.override.yaml   # 开发覆盖(自动加载)
├── compose.prod.yaml       # 生产覆盖
├── .env.dev                # 开发变量
├── .env.prod               # 生产变量
├── composer.json
├── composer.lock
├── Dockerfile
└── src/
    └── index.php

  • compose.yaml 放公共核心配置,用 profiles 标记可选服务。

  • compose.override.yaml 放开发专用配置(挂载、热重载、开发依赖)。

  • compose.prod.yaml 放生产专用配置(资源限制、重启策略)。

  • 环境变量用 .env 文件管理,通过 --env-file 指定。

开发时:

docker compose --env-file .env.dev up -d --build
# docker compose watch

生产时:

docker compose --env-file .env.prod -f compose.yaml -f compose.prod.yaml up -d --build

八、多文件合并的三条核心规则

理解合并规则能帮你避开很多坑:

  1. 对象(mapping)递归合并:同 key 的对象会深度合并,子 key 一起保留。

  2. 序列(sequence)整体替换:override 中的序列会替换 base 中的整个序列,而不是追加。

  3. 单向叠加,无法删除:你无法从 override 中移除 base 已有的 key。所以 base 里只放共有的,环境特有的放到覆盖文件。

常见陷阱:
  • 在 override 中重新定义 build 会丢失 base 的 build 子字段,必须连带写上。

  • environment、volumes、ports 等数组字段在 override 中要写全。

  • 指定 -f 后,compose.override.yaml 不会自动加载。

  • secrets 的文件路径相对于 compose 文件所在目录。

九、完整实操步骤

  1. 创建项目目录,进入。

  2. 创建 src/index.php,内容:<?php echo "Hello Docker!";

  3. 创建 Dockerfile,内容如第四部分所示。

  4. 创建 compose.yaml、compose.override.yaml、compose.prod.yaml,内容分别如上。

  5. 创建 .env.dev 和 .env.prod。

  6. 开发环境运行:

 docker compose --env-file .env.dev up -d --build
 # docker compose watch

修改 src/index.php,刷新页面看变化。

  1. 生产环境运行:

docker compose --env-file .env.prod -f compose.yaml -f compose.prod.yaml up -d --build

  1. 验证合并结果:

docker compose -f compose.yaml -f compose.prod.yaml config

总结

Docker Compose 多文件策略是管理多环境配置的标准方案。对于初学者,记住以下要点:

  • 基础文件 compose.yaml 只放公共配置。

  • 开发覆盖 compose.override.yaml 自动加载,开发时直接 docker compose up。

  • 生产覆盖 compose.prod.yaml 手动指定,用 -f 组合。

  • profiles 控制可选服务的启停。

  • 环境变量 用 .env 文件管理简单差异。

  • 合并规则:对象递归合并,序列整体替换,不能删除。

  • 指定 -f 后自动加载失效,生产环境必须显式写出所有文件。

开发环境:用 .env 文件设置数据库密码可以接受,方便快捷。

生产环境:强烈推荐使用 secrets,而不是 .env 或环境变量。