如果你刚接触 Docker,可能会觉得一个 compose.yaml 就能搞定所有事情。但随着项目从本地开发走向生产部署,你会发现:开发时需要热重载、调试工具,生产时却需要资源限制、自动重启。把这些配置全塞进一个文件,不仅混乱,还容易把开发专用功能误带到生产环境。Docker 官方推荐的多文件分层策略,正是解决这个问题的标准方法。本文专为有一点 Docker 基础的初学者设计,手把手带你从零搭建一个可实操的项目,彻底搞懂多文件、profiles 和环境变量的配合使用。
本文将带你完成一个 PHP + MariaDB + phpMyAdmin 的示例项目,并逐步将其改造成支持开发和生产两套环境的配置。你会学到:
如何用 compose.yaml + compose.override.yaml + compose.prod.yaml 拆分配置。
Docker Compose 的自动加载规则,以及 -f 参数的影响。
如何用 profiles 控制可选服务(如调试工具)的启停。
如何用 .env 文件管理环境变量。
多文件合并的核心规则与常见陷阱。
最终形成一套“多文件 + profiles + 环境变量”的最佳实践。
所有代码都可以直接复制运行,跟着做一遍就能掌握。
一、准备工作
开始之前,请务必安装并启动 Docker Desktop。
创建一个项目目录:
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 -d5. 自动加载规则(重要!)
当你执行 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.phpcompose.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八、多文件合并的三条核心规则
理解合并规则能帮你避开很多坑:
对象(mapping)递归合并:同 key 的对象会深度合并,子 key 一起保留。
序列(sequence)整体替换:override 中的序列会替换 base 中的整个序列,而不是追加。
单向叠加,无法删除:你无法从 override 中移除 base 已有的 key。所以 base 里只放共有的,环境特有的放到覆盖文件。
常见陷阱:
在 override 中重新定义 build 会丢失 base 的 build 子字段,必须连带写上。
environment、volumes、ports 等数组字段在 override 中要写全。
指定 -f 后,compose.override.yaml 不会自动加载。
secrets 的文件路径相对于 compose 文件所在目录。
九、完整实操步骤
创建项目目录,进入。
创建 src/index.php,内容:<?php echo "Hello Docker!";
创建 Dockerfile,内容如第四部分所示。
创建 compose.yaml、compose.override.yaml、compose.prod.yaml,内容分别如上。
创建 .env.dev 和 .env.prod。
开发环境运行:
docker compose --env-file .env.dev up -d --build
# docker compose watch修改 src/index.php,刷新页面看变化。
生产环境运行:
docker compose --env-file .env.prod -f compose.yaml -f compose.prod.yaml up -d --build验证合并结果:
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 或环境变量。