在使用 Docker Compose 部署多服务应用时,敏感信息管理、YAML 语法理解以及顶层配置项的正确使用,是开发者绕不开的三大核心问题。很多初学者困惑于:为什么密码文件在 Dockerfile 中找不到,容器却能访问?YAML 的三种数据结构到底只作用于“值”,还是所有节点?Compose 的顶层键究竟有几个?本文将以一个真实的 PHP + MariaDB + phpMyAdmin 项目为例,系统梳理这些知识点,纠正常见误解,并给出生产级实践建议。
一、Docker Compose Secrets 机制:运行时注入,与构建无关
1.1 为什么 Dockerfile 里找不到密码文件?
在项目 compose.yaml 中,顶层定义了 secret:
secrets:
db-password:
file: db/password.txt服务中引用:
services:
db:
secrets:
- db-password
environment:
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-passwordDockerfile 中没有任何 COPY db/password.txt 的操作,但容器内却能读取 /run/secrets/db-password。这是因为 Docker Compose 在运行时(docker compose up)将宿主机文件注入容器,完全绕过了 docker build 阶段。
1.2 核心机制:构建时 vs 运行时
阶段 | 命令 | 涉及文件 | 是否写入镜像 |
|---|---|---|---|
构建时 | docker build | Dockerfile 上下文中的文件 | 是,成为镜像层 |
运行时 | docker compose up | 宿主机 secret 文件 | 否,仅注入容器 |
Secret 注入流程:
Compose 解析顶层 secrets 定义,找到宿主机文件 db/password.txt。
创建容器时,向 Docker Engine 传递 secret 配置。
容器内出现 /run/secrets/db-password,内容即宿主机文件内容。
应用通过环境变量(如 MARIADB_ROOT_PASSWORD_FILE)得知文件路径,读取密码。
1.3 安全设计
不写入镜像:密码不会出现在镜像层,推送 Registry 时不泄露。
运行时挂载:单机 Compose 通常以只读 bind mount 方式挂载;Swarm 模式则使用 tmpfs 内存文件系统。
路径解析:file: db/password.txt 相对于 compose.yaml 所在目录。
1.4 密码修改与轮换
修改宿主机密码文件后,运行中的容器不会自动更新。 必须重建容器:
docker compose up -d --force-recreate更关键的问题:数据库首次初始化时,MARIADB_ROOT_PASSWORD_FILE 才生效。一旦 db-data volume 存在,密码已写入 MySQL 系统表,修改 secret 不会自动更改数据库密码。
开发环境:删除 volume 重新初始化:
docker compose down -v
docker compose up -d生产环境方案:
方案 A:轮换脚本——启动临时容器执行 ALTER USER。
方案 B:版本化文件——每次使用新文件名,实现秒级回滚。
方案 C:外部 Secret Manager——如 Vault、AWS Secrets Manager,需 Swarm 或 Kubernetes 支持。
示例:版本化命名
不覆盖旧密码文件,每次轮换创建新的版本,完整流程:
# ===== 第一步:创建新版本密码文件 =====
echo "newpass" > db/password.v2.txt
# ===== 第二步:在数据库内修改密码 =====
# 这一步是必须的,无法跳过
docker compose exec db mariadb -u root -pdemo -e \
"ALTER USER 'root'@'localhost' IDENTIFIED BY 'newpass'; \
ALTER USER 'root'@'%' IDENTIFIED BY 'newpass'; \
FLUSH PRIVILEGES;"
# ===== 第三步:修改 compose.yaml =====
# 将所有 db-password-v1 → db-password-v2
# 将 file: db/password.v1.txt → file: db/password.v2.txt
# ===== 第四步:重建所有相关容器 =====
docker compose up -d --force-recreate回滚流程:
# 出问题了!需要回滚
# 1. 在数据库内改回旧密码
docker compose exec db mariadb -u root -pnewpass -e \
"ALTER USER 'root'@'localhost' IDENTIFIED BY 'demo'; \
ALTER USER 'root'@'%' IDENTIFIED BY 'demo'; \
FLUSH PRIVILEGES;"
# 2. 修改 compose.yaml,改回 db-password-v1
# 旧文件 db/password.v1.txt 还在!不需要重新创建
# 3. 重建容器
docker compose up -d --force-recreate优势:旧版本文件保留,出问题可以立即回滚。
最小权限原则:避免使用 root,创建应用专用账号。
db:
environment:
- MARIADB_USER=appuser # 应用专用账号
- MARIADB_PASSWORD_FILE=/run/secrets/db-password
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-root-password # root 仅管理用二、YAML 语法:节点树的三种基本结构
2.1 YAML 是一棵节点树
YAML 文档本质上是一棵节点树。每个节点只能是三种结构之一:
结构 | 说明 | 写法示例 |
|---|---|---|
标量 (Scalar) | 单个值(字符串、数字、布尔) | image: mariadb |
映射 (Mapping) | 键值对集合(字典/对象) | db: { condition: service_healthy } |
序列 (Sequence) | 有序列表(数组) | - db 或 [db] |
在 YAML 中,字符串不是必须加引号的,引号只在需要消除歧义或明确类型时才必须使用。
示例:下面这种写法都合法
流式映射(Flow Mapping)
depends_on:
db: { condition: service_healthy }块式写法
depends_on:
db:
condition: service_healthy2.2 三种结构作用于所有节点,不限于“值”
Compose 的键一般是标量字符串,平时只需留意值的类型即可。不过从规范上讲,根节点、嵌套映射和序列元素都可以是任意结构。
services: # 根映射的键,其值是映射
server: # services 映射的键,其值是映射
ports: # server 映射的键,其值是序列
- "9000:80" # 序列元素是标量对应节点树:
根节点(映射)
├── services(映射)
│ ├── server(映射)
│ │ ├── ports(序列)
│ │ │ └── "9000:80"(标量)2.3 块序列 vs 流序列
块序列:用 - 开头,适合长列表。
流序列:用 [],适合短列表。
两者功能等价,仅是风格差异。示例:
块序列
healthcheck:
test:
[
"CMD",
"/usr/local/bin/healthcheck.sh",
"--su-mysql",
"--connect",
"--innodb_initialized",
]流序列
healthcheck:
test:
- CMD
- /usr/local/bin/healthcheck.sh
- --su-mysql
- --connect
- --innodb_initialized2.4 depends_on 的语义差异
# 映射形式:指定条件
depends_on:
db:
condition: service_healthy # 附加条件:等 db 健康后才启动
# 序列形式:仅声明依赖
depends_on:
- db # 仅声明依赖,无条件(默认 service_started)三、Docker Compose 顶层键
当前 Compose 规范定义的顶层键共9个:
顶层键 | 是否必填 | 说明 |
|---|---|---|
version | 否(已废弃) | 仅向后兼容,Compose 会警告,不再用于 schema 选择 |
name | 否 | 自定义项目名称 |
include | 否 | 引入其他 Compose 文件,实现模块化 |
services | 是 | 定义容器服务,唯一必填项 |
models | 否 | 新增,声明 AI 模型依赖(需 Compose 2.38.0+) |
networks | 否 | 定义命名网络 |
volumes | 否 | 定义持久化数据卷 |
secrets | 否 | 定义敏感信息 |
configs | 否 | 定义只读配置文件 |
注意:服务内的 secrets 是列表(引用顶层定义),而顶层 secrets 是映射(定义 secret 本身)。
四、实战:示例解析
示例项目结构
├── docker-php-sample/
│ ├── db/
│ │ └──password.txt
│ ├── src/
│ │ └──database.php
│ │ └──hello.php
│ ├── tests/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── compose.yaml
│ ├── composer.json
│ ├── composer.lock
│ ├── Dockerfile
│ ├── README.mdcompose.yaml
# ============================================================
# Docker Compose 配置文件 - PHP + MariaDB + phpMyAdmin 示例
# ============================================================
# 本文件演示了 Docker Compose 的核心功能:
# - 多服务编排(PHP 应用、MariaDB 数据库、phpMyAdmin 管理界面)
# - 服务依赖与健康检查
# - Docker Secrets 安全管理敏感数据
# - 持久化卷(Volumes)
# ============================================================
# ------------------------------------------------------------
# services: 定义所有容器服务
# ------------------------------------------------------------
services:
# ----------------------------------------------------------
# server 服务:自定义 PHP 应用容器
# ----------------------------------------------------------
server:
build:
context: . # 构建上下文为当前目录,使用当前目录下的 Dockerfile 构建镜像
ports:
- "9000:80" # 端口映射:宿主机 9000 端口 → 容器内 80 端口(访问 localhost:9000 即可到达应用)
depends_on:
db:
condition: service_healthy # 依赖 db 服务,且必须等 db 的健康检查通过后才启动 server
secrets:
- db-password # 引用顶层 secrets 中定义的 db-password,该 secret 会以文件形式挂载到容器的 /run/secrets/db-password
environment:
- PASSWORD_FILE_PATH=/run/secrets/db-password # 告诉应用从 /run/secrets/db-password 文件中读取数据库密码
- DB_HOST=db # 数据库主机名,对应 db 服务的服务名(Docker 内部 DNS 自动解析)
- DB_NAME=example # 数据库名称
- DB_USER=root # 数据库用户名
# ----------------------------------------------------------
# db 服务:MariaDB 数据库容器
# ----------------------------------------------------------
db:
image: mariadb # 使用官方 mariadb 镜像
restart: always # 容器退出时始终自动重启(适用于生产环境的数据库服务)
user: root # 以 root 用户身份运行容器内的主进程
secrets:
- db-password # 引用 db-password secret,注入到 db 容器的 /run/secrets/db-password
volumes:
- db-data:/var/lib/mysql # 持久化卷映射:将命名卷 db-data 挂载到容器内 MySQL 数据目录,防止容器重建后数据丢失
environment:
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/db-password # MariaDB 官方镜像支持 _FILE 后缀,从文件读取 root 密码,避免在环境变量中明文暴露密码
- MARIADB_DATABASE=example # 容器首次启动时自动创建名为 example 的数据库
expose:
- 3306 # 仅在 Docker 内部网络暴露 3306 端口,不映射到宿主机(仅其他容器可访问)
healthcheck:
test:
[
"CMD", # 健康检查方式:在容器内执行命令
"/usr/local/bin/healthcheck.sh", # MariaDB 官方镜像自带的健康检查脚本
"--su-mysql", # 以 mysql 用户身份执行检查
"--connect", # 检查是否能成功连接到数据库
"--innodb_initialized", # 检查 InnoDB 存储引擎是否已初始化完成
]
interval: 10s # 每隔 10 秒执行一次健康检查
timeout: 5s # 单次健康检查的超时时间为 5 秒
retries: 5 # 连续 5 次检查失败后,才将容器标记为 unhealthy
# ----------------------------------------------------------
# phpmyadmin 服务:数据库可视化管理界面
# ----------------------------------------------------------
phpmyadmin:
image: phpmyadmin # 使用官方 phpmyadmin 镜像
ports:
- "9090:80" # 端口映射:宿主机 9090 端口 → 容器内 80 端口(访问 localhost:9090 打开 phpMyAdmin 界面)
depends_on:
- db # 依赖 db 服务(仅确保启动顺序,不等待健康检查)
environment:
- PMA_HOST=db # 指定 phpMyAdmin 连接的数据库主机名为 db(对应 db 服务的服务名)
# ------------------------------------------------------------
# volumes: 定义命名卷,用于数据持久化
# ------------------------------------------------------------
volumes:
db-data: # 命名卷 db-data,由 Docker 自动管理存储位置,用于保存 MariaDB 的数据文件
# ------------------------------------------------------------
# secrets: 顶层 secrets 定义,从宿主机文件读取敏感数据
# ------------------------------------------------------------
# Docker Secrets 是管理敏感信息(如密码、证书、API Key)的安全机制:
# - Secret 以只读文件形式挂载到容器的 /run/secrets/<secret名> 目录
# - 避免在 compose 文件或环境变量中明文写入密码
# - 与环境变量相比,secrets 不会被 docker inspect 轻易暴露
# ------------------------------------------------------------
secrets:
db-password: # 定义名为 db-password 的 secret
file: db/password.txt # 从宿主机 compose.yaml 同目录下的 db/password.txt 文件读取 secret 的内容Dockerfile
FROM composer:lts as deps
WORKDIR /app
RUN --mount=type=bind,source=composer.json,target=composer.json \
--mount=type=bind,source=composer.lock,target=composer.lock \
--mount=type=cache,target=/tmp/cache \
composer install --no-dev --no-interaction
FROM php:8.2-apache as final
RUN docker-php-ext-install pdo pdo_mysql
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --from=deps app/vendor /var/www/html/vendor
COPY ./src /var/www/html/
USER www-data五、常见误区与验证方法
常见误区
误以为 secrets 会写入镜像:不会,运行时注入。
误以为修改密码文件后数据库密码自动更新:不会,需手动轮换或重建 volume。
误以为 YAML 三种结构只针对值:它们作用于所有节点。
验证工具
docker compose config:规范化输出,验证字段类型。
总结
本文系统梳理了 Docker Compose 的三大核心知识:
Secrets 机制:运行时从宿主机注入容器,不经过 Dockerfile 构建,不写入镜像;修改密码后需重建容器,数据库密码需通过轮换脚本或删除 volume 更新。
YAML 节点树:文档是一棵节点树,每个节点可为标量、映射或序列,三种结构递归嵌套,不限于“值”。
顶层键规范:当前共 9 个顶层键(version 废弃、name、include、services 必填、models、networks、volumes、secrets、configs)。