Docker Compose 从项目模型、生命周期到商业编排
Docker Compose 不只是“把多条 docker run 写进一个 YAML”。它会把一个应用栈解析成项目模型,再调用 Docker Engine 创建容器、网络、Volume、Secret 和其他对象。真正需要掌握的是:项目名如何决定资源名、配置如何插值和合并、up 为什么会重建容器、depends_on 为什么不能代替应用重试、服务名为什么能做 DNS、down -v 为什么可能删除数据,以及 Compose 到底适不适合生产。
学习目标
学完本页,应能:
- 解释 Compose CLI、Compose Model、Docker Engine 和容器运行时之间的调用关系。
- 区分 service、container、project、network、volume、config、secret。
- 解释项目名、服务名、容器名、DNS 名和资源名前缀的关系。
- 看懂
up、start、stop、restart、down、run、exec的行为差异。 - 分清 YAML 变量插值、
env_file、environment、镜像ENV和最终容器环境。 - 正确使用健康检查和
depends_on.condition,同时在应用层实现超时、重试和熔断。 - 设计 Compose 网络、端口、Volume、Secret、Profile 和多文件覆盖。
- 写出 Spring Boot、MySQL、Redis 的可运行商业场景 Demo。
- 从配置错误、依赖未就绪、DNS、端口、权限、重建和数据丢失等现象定位根因。
- 说明 Compose 与 Docker Swarm、Kubernetes 的边界。
一、Compose解决的是“一个应用栈如何被声明和重复创建”
没有 Compose 时,开发者可能手工执行:
docker network create order-net
docker volume create order-mysql-data
docker run -d --name mysql --network order-net ... mysql:8.0
docker run -d --name redis --network order-net ... redis:7
docker run -d --name order-api --network order-net -p 8080:8080 ... order-api:1.0这组命令的问题不是“太长”这么简单:
- 参数散落在 Shell 历史、Wiki 和个人电脑中。
- 很难判断某个容器当前配置是否和团队约定一致。
- 容器创建顺序、网络、Volume、健康检查容易遗漏。
- 改一次配置后,不知道应该重启还是重建哪些容器。
- 不同开发者可能创建同名资源并相互干扰。
Compose 将期望状态写进 compose.yaml:
项目需要哪些服务
每个服务使用什么镜像
如何构建
加入哪些网络
挂载哪些数据
暴露哪些端口
使用什么环境变量
有什么启动依赖
如何判断健康
如何停止和重启它让配置可以进入 Git、代码评审和自动化流程,但不会自动使架构具备高可用、备份、滚动发布或跨主机调度能力。
二、Compose V1、V2与Compose Specification
历史上常见命令是:
docker-compose up -d这是带连字符的独立 Compose V1 工具。现代 Docker 通常使用 Compose V2 插件:
docker compose up -d推荐以当前 Compose Specification 和实际安装的 V2 版本为准:
docker compose version
docker version旧文章中的顶层:
version: "3.8"在现代 Compose Specification 中通常不再用于选择一套互斥语法版本,甚至可能收到“obsolete”提示。团队应通过工具版本、CI 校验和 docker compose config 确认功能支持,不能只凭 version: "3.8" 判断行为。
三、Compose工作原理:YAML不会直接变成进程
flowchart TD
A["compose.yaml与覆盖文件"] --> B["Compose CLI读取配置"]
B --> C["变量插值与文件合并"]
C --> D["校验并生成Compose项目模型"]
D --> E["比较期望模型和现有资源"]
E --> F["调用Docker Engine API"]
F --> G["创建或复用Network与Volume"]
F --> H["拉取镜像或执行build"]
F --> I["创建或重建Container"]
I --> J["containerd与OCI runtime启动进程"]Compose 本身不是新的容器运行时。它是面向“多服务项目”的客户端编排层:
- 读取配置。
- 解析成统一项目模型。
- 调用 Docker Engine API。
- Docker daemon 再负责镜像、容器、网络和 Volume。
- 最终仍由 containerd、OCI runtime 和 Linux 内核运行容器进程。
因此,docker compose ps 中的容器也能被普通命令查看:
docker ps
docker inspect <容器名>
docker network inspect <项目网络名>
docker volume inspect <项目卷名>四、先分清Project、Service和Container
4.1 Project是资源隔离边界
假设目录名为 order-platform,配置为:
services:
api:
image: order-api:1.0
mysql:
image: mysql:8.0Compose 通常会创建类似:
order-platform-api-1
order-platform-mysql-1
order-platform_default其中:
order-platform是项目名。api、mysql是服务名。order-platform-api-1是某个服务实例对应的容器名。order-platform_default是项目默认网络。
4.2 项目名从哪里来
常见控制方式:
docker compose -p order-dev up -d或者:
COMPOSE_PROJECT_NAME=order-dev docker compose up -d也可以在配置中声明:
name: order-dev若都没有,通常根据 Compose 项目目录名推导。实际优先级和特殊多文件场景应使用:
docker compose config
docker compose ls确认,不要凭容器名前缀猜测。
4.3 为什么项目名改变后“数据库数据不见了”
配置:
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:项目名是 order-dev 时,实际命名卷可能是:
order-dev_mysql-data项目名改成 order-test 后,Compose 创建:
order-test_mysql-dataMySQL 挂载的是新的空卷,看起来像“原数据丢了”,但旧卷可能仍存在。排查:
docker compose ls
docker volume ls
docker compose config
docker inspect <mysql容器>4.4 为什么不建议滥用container_name
services:
api:
container_name: order-api硬编码容器名会:
- 让不同 Compose 项目更容易发生名称冲突。
- 破坏按项目名自动隔离的效果。
- 通常阻碍同一服务扩展多个副本,因为容器名必须唯一。
- 让调用方错误依赖某个实例名,而不是服务发现名称。
服务间通信应优先使用服务名 api、mysql,而不是手工固定容器名或 IP。
五、Compose文件结构
最常见顶层对象:
name: order-platform
services:
api: {}
mysql: {}
networks:
backend: {}
volumes:
mysql-data: {}
secrets:
mysql-password: {}
configs:
nginx-conf: {}| 顶层对象 | 解决的问题 |
|---|---|
services | 定义可创建一个或多个容器实例的服务模板 |
networks | 定义容器网络连接关系 |
volumes | 定义持久数据对象 |
secrets | 定义需要以文件等方式授予服务的敏感输入 |
configs | 定义非敏感配置文件 |
name | 明确项目名 |
Service 不是正在运行的容器。Service 是容器配置模板;容器是这个模板创建出来的实例。
六、先用docker compose config看“最终配置”
原始 YAML 不一定等于最终生效配置,因为中间还可能发生:
- 环境变量插值。
.env或--env-file读取。- 多个
-f文件合并。 - profiles 过滤。
- YAML 锚点展开。
- 相对路径解析。
- Compose 默认值补全。
因此修改后先执行:
docker compose config
docker compose config --services
docker compose config --volumes
docker compose config --profiles仅校验并安静退出:
docker compose config --quiet注意:展开后的配置可能包含环境变量中的密码或 Token,复制到工单和群聊前必须脱敏。
七、变量插值与容器环境变量是两条链路
这是 Compose 最常见的误区。
7.1 第一条链路:Compose解析YAML时插值
配置:
services:
api:
image: "order-api:${APP_VERSION:-latest}"
ports:
- "${HOST_PORT:-8080}:8080"Compose 在创建容器之前先替换 ${APP_VERSION} 和 ${HOST_PORT}。这些值可能来自当前 Shell、.env 或显式 --env-file。
查看插值环境:
docker compose config --environment
docker compose config常用语法:
image: "order-api:${APP_VERSION:-latest}" # 未设置或为空时用latest
image: "order-api:${APP_VERSION-default}" # 未设置时用default
image: "order-api:${APP_VERSION:?must be set}" # 缺失时直接报错需要把 $ 原样传进容器命令而不让 Compose 提前插值时,常用双写:
healthcheck:
test: ["CMD-SHELL", "echo $$HOSTNAME"]${变量} 是 Compose 解析阶段;$$变量 通常用于延迟到容器内 Shell 再展开。
7.2 第二条链路:给容器设置环境变量
services:
api:
env_file:
- ./app.env
environment:
SPRING_PROFILES_ACTIVE: dev
LOG_LEVEL: INFO这些值进入容器 .Config.Env,应用进程才可能读取:
docker compose up -d
docker compose exec api env
docker inspect --format '{{json .Config.Env}}' <api容器>7.3 .env不等于env_file
| 文件 | 主要作用 |
|---|---|
项目 .env | 为 Compose 文件本身提供插值值,也可能影响 Compose CLI 行为 |
service 的 env_file | 将变量放入指定服务容器的环境 |
一个值出现在 .env 中,不代表它一定进入容器;必须在 Compose 最终模型中被引用,或者通过 env_file、environment 等注入。
7.4 最终值冲突怎么判断
不要靠记忆猜复杂优先级。分两步验证:
# 第一步:Compose解析和合并后的模型
docker compose config
# 第二步:容器实际获得的环境
docker compose up -d
docker inspect --format '{{json .Config.Env}}' <容器>一般应明确这些来源:
- 镜像 Dockerfile 的
ENV默认值。 - service
env_file。 - service
environment。 - Compose 插值所使用的 Shell、
.env、--env-file。 docker compose run -e等命令行覆盖。
敏感值不应因为“优先级清楚”就放入普通环境变量;环境变量可能被 inspect、进程信息、诊断包和日志看到。
八、up并不只是“start所有容器”
执行:
docker compose up -dCompose 大致会:
flowchart TD
A["解析最终项目模型"] --> B["创建缺失的网络和Volume"]
B --> C["拉取镜像或执行build"]
C --> D{"服务容器是否存在"}
D -- "不存在" --> E["创建容器"]
D -- "存在" --> F{"配置或镜像是否需要更新"}
F -- "需要" --> G["停止并重建容器"]
F -- "不需要" --> H["复用现有容器"]
E --> I["按依赖关系启动"]
G --> I
H --> I
I --> J["前台聚合日志或后台返回"]常用选项:
docker compose up -d
docker compose up --build -d
docker compose up --pull always -d
docker compose up --force-recreate -d
docker compose up --no-recreate -d
docker compose up --remove-orphans -d
docker compose up --wait -d| 选项 | 含义与风险 |
|---|---|
--build | 启动前构建有 build 配置的服务,不表示一定忽略缓存 |
--pull always | 尝试拉取镜像,适合明确需要检查远端更新的流程 |
--force-recreate | 即使配置看起来没变也重建容器,会造成服务中断 |
--no-recreate | 已存在容器不重建,可能导致新配置没有生效 |
--remove-orphans | 删除不在当前模型中的同项目容器,执行前确认项目名和覆盖文件 |
--wait | 等待服务达到 running/healthy,要求健康定义合理且版本支持 |
8.1 重建容器不等于删除命名卷
重建会删除旧容器及其可写层,再基于镜像创建新容器。显式命名卷通常重新挂载,因此数据可以保留;但容器可写层中的数据会消失。
这也是为什么应用不能把数据库、上传文件或唯一业务数据只写在容器可写层。
8.2 镜像标签不变也可能产生认知混乱
如果一直使用:
image: order-api:latest本地已有 latest 与远端 latest 可能不是同一内容。是否拉取、何时重建、当前容器使用哪个 Image ID 都要检查:
docker compose images
docker compose pull
docker inspect --format '{{.Image}}' <容器>
docker image inspect order-api:latest生产更适合使用版本、commit Tag,并在部署记录中保存 Digest。
九、生命周期命令必须分清
| 命令 | 核心行为 | 容器 | 默认网络 | 命名卷 |
|---|---|---|---|---|
create | 创建但不启动 | 创建 | 创建 | 创建或复用 |
start | 启动已有容器 | 保留 | 保留 | 保留 |
stop | 发停止信号并等待 | 保留 | 保留 | 保留 |
restart | 停止后再启动已有容器 | 保留 | 保留 | 保留 |
kill | 发送指定信号,默认强杀 | 保留 | 保留 | 保留 |
rm | 删除已停止服务容器 | 删除 | 通常保留 | 默认不删命名卷 |
down | 删除项目容器和默认网络 | 删除 | 删除 | 默认保留命名卷 |
down -v | 在 down 基础上删除声明卷 | 删除 | 删除 | 可能删除,危险 |
停止:
docker compose stop -t 30再次启动同一批容器:
docker compose start删除项目容器和网络但保留普通命名卷:
docker compose down删除项目并同时处理 Volume:
docker compose down -v对数据库环境执行
down -v前,必须确认项目名、最终配置、目标卷、备份和恢复能力。它不是普通“彻底停止”命令。
9.1 restart不会重新读取所有配置
docker compose restart 主要重启已有容器,不等于按新 Compose 模型重建。修改环境变量、挂载、端口或镜像配置后,通常需要 up -d 触发重建,而不是只执行 restart。
验证配置是否真正进入容器:
docker compose config
docker compose up -d
docker inspect <容器>十、run与exec不是一回事
在已有 api 容器中执行命令:
docker compose exec api shexec 要求服务容器正在运行,它在该容器中创建额外进程,共享其文件系统、网络和挂载。
根据 service 配置创建一次性新容器:
docker compose run --rm api java -versionrun 会创建新的 one-off 容器,默认行为与端口发布等细节可能不同,应显式检查需要的选项。例如一次性数据库迁移:
docker compose run --rm migration不要把 run 当成“进入当前线上容器”,否则观察到的 PID、可写层和临时文件不是原实例现场。
十一、服务发现与网络原理
默认情况下,Compose 为项目创建默认 bridge 网络,并将服务连接进去:
services:
api:
image: order-api:1.0
mysql:
image: mysql:8.0api 访问 MySQL 应使用:
mysql:3306而不是:
localhost:3306因为每个容器拥有独立 Network Namespace,localhost 只指向当前容器自己。
11.1 服务名为什么能解析
Compose 将网络端点和服务名/别名交给 Docker 网络系统。连接到同一用户自定义网络的容器,可以通过 Docker 内置 DNS 解析服务名。
flowchart TD
A["api查询mysql"] --> B["容器DNS配置"]
B --> C["Docker内置DNS"]
C --> D["找到mysql服务网络端点"]
D --> E["返回当前容器IP"]
E --> F["api连接mysql容器3306"]容器重建后 IP 可能变化,但服务名可以继续解析到新端点。因此客户端不应缓存容器 IP 到永久配置。
11.2 自定义前后端网络
services:
gateway:
image: nginx:1.27
networks:
- frontend
- backend
api:
image: order-api:1.0
networks:
- backend
mysql:
image: mysql:8.0
networks:
- backend
networks:
frontend:
backend:
internal: trueinternal: true 可限制该网络的外部连通能力,但不能替代应用认证、数据库账号权限和宿主机防火墙。gateway 同时连接两个网络,用于把入口请求转给 backend 中的 api。
11.3 网络别名
services:
api:
image: order-api:1.0
networks:
backend:
aliases:
- order.internal
networks:
backend:别名只在对应网络范围内有意义。不要误认为它会自动注册到企业 DNS 或公网 DNS。
十二、ports、expose与容器内部端口
services:
api:
ports:
- "127.0.0.1:18080:8080"含义:
- 应用在容器内监听 8080。
- Docker 将宿主机
127.0.0.1:18080转发到容器 8080。 - 只有宿主机本地可以直接通过该绑定地址访问。
如果写:
ports:
- "18080:8080"通常绑定所有宿主机接口,是否能从外部访问还受宿主机防火墙、云安全组和路由影响。
expose:
expose:
- "8080"用于表达服务内部端口,不会创建宿主机端口映射。同网络容器原本就可以直接访问目标容器实际监听端口,expose 不是网络 ACL。
数据库仅供 api 使用时,通常没必要发布到宿主机:
services:
mysql:
image: mysql:8.0
# 不配置ports,api仍可通过mysql:3306访问这样可以减少宿主机暴露端口和端口冲突。
十三、depends_on为什么不能代替应用重试
短语法:
services:
api:
depends_on:
- mysql主要表达启动顺序:先启动 mysql 容器,再启动 api 容器。但“mysql 容器主进程已经启动”不等于“MySQL 已经完成初始化并能接受业务连接”。
13.1 健康依赖
services:
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
api:
image: order-api:1.0
depends_on:
mysql:
condition: service_healthy常见条件:
| 条件 | 含义 |
|---|---|
service_started | 依赖容器已启动,不保证应用可服务 |
service_healthy | 等待依赖健康检查通过 |
service_completed_successfully | 等待一次性任务成功结束,适合迁移任务等场景 |
具体条件和附加字段支持与 Compose 版本有关,应通过 docker compose version 和 docker compose config 验证。
13.2 为什么有healthcheck仍要应用重试
健康依赖只解决初始启动的一部分问题。运行过程中仍可能发生:
- MySQL 重启。
- 网络瞬时抖动。
- DNS 更新。
- 主从切换。
- 连接池中的旧连接失效。
- Redis、MQ 或第三方接口短暂不可用。
应用必须设置连接超时、有限重试、指数退避、熔断和业务降级。无限快速重试会在依赖恢复前制造重试风暴。
13.3 健康检查不能只看进程存在
一个 Java 进程可能仍在运行,但已经:
- Full GC。
- 线程池耗尽。
- 数据库连接池耗尽。
- 核心配置加载失败。
- 关键业务初始化未完成。
健康检查应选择低成本、语义明确的 endpoint,并区分“进程活着”和“可以接流量”。检查工具必须真实存在于镜像中,不能照抄 curl 后才发现 JRE 镜像没有 curl。
十四、重启策略与依赖重启不是一回事
services:
api:
restart: unless-stopped常见策略:
| 策略 | 行为 |
|---|---|
no | 默认,不自动重启 |
on-failure | 非零退出时重启,可按环境限制次数 |
always | 进程退出后持续尝试重启 |
unless-stopped | 除非显式停止,否则在 daemon 重启等情况下恢复 |
重启策略不能修复应用启动失败。配置错误时 always 可能造成高频重启和日志刷屏,应配合:
docker compose ps
docker inspect --format '{{.RestartCount}} {{json .State}}' <容器>
docker compose logs --tail 200 apidepends_on 长语法中的 restart: true 描述的是某些 Compose 显式更新依赖后的联动行为,不等同于容器运行时 restart: always,也不应假定依赖每次崩溃重启都会自动重启所有调用方。
十五、Volume生命周期
15.1 命名卷
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:通常创建带项目名前缀的命名卷。docker compose down 默认保留它,down -v 可能删除。
15.2 Bind Mount
services:
api:
volumes:
- type: bind
source: ./config
target: /app/config
read_only: true相对路径依赖 Compose 项目文件位置和多文件基准规则。Bind Mount 还受宿主机目录是否存在、UID/GID、SELinux 和 Docker Desktop 文件共享影响。
15.3 外部卷
volumes:
mysql-data:
external: true
name: order-mysql-prodCompose 不负责创建一个不存在的 external volume,项目删除时也不会把它当作普通项目卷删除。它仍不是备份,也不自动高可用。
15.4 挂载为什么遮住镜像文件
若镜像 /app/config 已有默认文件,再把宿主机空目录挂到同一路径,容器看到的是挂载目录,原镜像文件被遮住。这不是文件被 Compose 删除,而是 Mount 改变了该路径的可见文件系统。
详细原理见 Docker存储挂载与数据生命周期。
十六、Secret与Config的真实边界
本地 Compose Secret 示例:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password
secrets:
- mysql-root-password
secrets:
mysql-root-password:
file: ./secrets/mysql-root-password.txt容器中通常以文件形式出现:
/run/secrets/mysql-root-password优点是密码不直接写进 Compose YAML 和普通容器环境变量。但要正确理解:
- 本地源文件仍需要安全保存和权限控制。
- 普通本地 Compose 不会因为写了
secrets就自动获得企业级密钥加密、轮换和审计。 - 目标镜像或应用必须支持从文件读取,例如 MySQL 官方镜像常见
_FILE变量。 - Secret 不能提交到 Git。
- 不能在启动日志中打印 Secret 内容。
非敏感配置可以使用 configs 或只读 Bind Mount。生产密钥更适合 Vault、云 Secret Manager、Kubernetes Secret 配合加密和 RBAC 等受控系统。
十七、多文件覆盖与合并
基础文件:
# compose.yaml
services:
api:
image: "order-api:${APP_VERSION}"
environment:
SPRING_PROFILES_ACTIVE: base
networks:
- backend
networks:
backend:开发覆盖:
# compose.dev.yaml
services:
api:
environment:
SPRING_PROFILES_ACTIVE: dev
ports:
- "8080:8080"运行:
docker compose -f compose.yaml -f compose.dev.yaml config
docker compose -f compose.yaml -f compose.dev.yaml up -d不要把合并简单理解为文本覆盖。不同字段可能:
- 按映射键合并。
- 追加或去重序列。
- 用后一个值替换前一个值。
- 对
command、entrypoint等采用特定规则。
另外,多文件中的相对路径通常以基础 Compose 文件的目录作为重要基准,不一定相对每个覆盖文件自身。最可靠方法仍是查看:
docker compose -f compose.yaml -f compose.dev.yaml config17.1 为什么覆盖文件可能让端口重复
如果基础文件和覆盖文件都追加了端口,而不是得到预期替换,最终模型可能包含多个端口映射。不要只看第二个文件,要看合并结果。
17.2 YAML锚点只做YAML复用
x-common: &common
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
services:
api:
<<: *common
image: order-api:1.0锚点是 YAML 层的复用机制,不等于 Compose 服务继承。过度嵌套锚点会让最终配置难以审查,仍应以 docker compose config 展开检查。
十八、Profiles管理可选服务
services:
api:
image: order-api:1.0
adminer:
image: adminer:4
profiles:
- debug
ports:
- "18081:8080"默认启动:
docker compose up -d启用调试服务:
docker compose --profile debug up -d适合:
- 本地数据库管理工具。
- 调试代理。
- 一次性数据准备工具。
- 可选监控组件。
不要用 Profile 隐藏关键安全控制,也不要让不同环境启用组合完全不可追踪。
十九、Build、Image与Pull策略
services:
api:
build:
context: .
dockerfile: Dockerfile
target: runtime
args:
GIT_COMMIT: "${GIT_COMMIT:?required}"
image: "registry.example.com/order/order-api:${APP_VERSION:?required}"这里:
build描述如何构建。image描述构建结果标签或要使用的镜像引用。- 是否构建、是否拉取以及优先行为还受命令选项和 pull policy 影响。
常用流程:
docker compose build --pull
docker compose push
docker compose pull
docker compose up -d生产环境更常见的是 CI 构建并推送不可变镜像,部署节点只拉取已经测试和扫描的镜像,而不是在生产服务器临时从源码构建。
二十、资源限制与日志
本地 Compose 常见:
services:
api:
cpus: 1.5
mem_limit: 1g
pids_limit: 300
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"不同 Compose/Engine 版本对 deploy.resources 等字段在非 Swarm 模式下的支持历史上存在差异。不要因为 YAML 能解析就假定 cgroup 限制已生效,要检查最终容器:
docker compose up -d
docker inspect --format 'memory={{.HostConfig.Memory}} nanoCpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}' <容器>
docker stats --no-stream日志方面:
- 应用优先输出 stdout/stderr。
- 配置 Docker 日志驱动轮转。
- 不要让无限增长的 json-file 写满 Docker 数据目录。
- 生产应把日志采集到集中系统,并附带项目、服务、实例和 traceId。
二十一、扩容的能力与边界
docker compose up -d --scale api=3前提:
- 不要硬编码同一个
container_name。 - 不要给每个副本绑定同一个宿主机固定端口。
- 应用状态要外置或支持多实例。
- 调用方必须能发现多个实例或通过反向代理进入。
普通 Compose 扩展多个容器,不等于完整的集群调度和高可用:
- 通常仍在单台 Docker 主机。
- 宿主机宕机后所有副本一起消失。
- 没有等同 Kubernetes Deployment 的滚动发布控制。
- 没有完整跨节点调度、自愈、Service 和存储编排语义。
因此“副本数是 3”不等于系统具有三节点容灾。
二十二、可运行商业Demo:Spring Boot、MySQL、Redis
目录:
order-stack/
├── compose.yaml
├── .env.example
├── .env
└── order-api/
├── Dockerfile
├── pom.xml
└── src/22.1 本地学习用.env
APP_VERSION=dev
APP_PORT=8080
MYSQL_DATABASE=order_db
MYSQL_USER=order_app
MYSQL_PASSWORD=local_only_password
MYSQL_ROOT_PASSWORD=local_only_root_password提交仓库的是 .env.example,真实 .env 加入 .gitignore。这个做法只适合本地学习和受控开发环境,不是生产密钥方案。
22.2 完整compose.yaml
name: order-stack
services:
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: "${MYSQL_DATABASE:?MYSQL_DATABASE is required}"
MYSQL_USER: "${MYSQL_USER:?MYSQL_USER is required}"
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}"
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required}"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
volumes:
- mysql-data:/var/lib/mysql
networks:
- backend
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
redis:
image: redis:7.2
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
api:
build:
context: ./order-api
dockerfile: Dockerfile
image: "order-api:${APP_VERSION:-dev}"
environment:
SPRING_PROFILES_ACTIVE: compose
SPRING_DATASOURCE_URL: "jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"
SPRING_DATASOURCE_USERNAME: "${MYSQL_USER}"
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: "6379"
ports:
- "127.0.0.1:${APP_PORT:-8080}:8080"
networks:
- backend
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
stop_grace_period: 30s
mem_limit: 1g
cpus: 1.5
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
adminer:
image: adminer:4
profiles:
- debug
ports:
- "127.0.0.1:18081:8080"
networks:
- backend
depends_on:
mysql:
condition: service_healthy
networks:
backend:
internal: false
volumes:
mysql-data:
redis-data:backend.internal 在本地 Demo 中设为 false,因为构建和运行环境可能需要正常外部访问;生产网络边界应按入口、依赖和运维需求单独设计,不能机械改成 true。
22.3 启动前验证
Copy-Item .env.example .env
docker compose config --quiet
docker compose config
docker compose build
docker compose up -d --waitLinux/macOS 复制命令:
cp .env.example .env22.4 启动后验证
docker compose ps
docker compose images
docker compose top
docker compose logs --tail 200 mysql
docker compose logs --tail 200 redis
docker compose logs --tail 200 api
curl -fsS http://127.0.0.1:8080/actuator/health确认服务名解析:
docker compose exec api getent hosts mysql
docker compose exec api getent hosts redis极简 Java 镜像可能没有 getent、curl 或 Shell。工具不存在不等于网络失败,可以:
- 从同网络启动临时诊断容器。
- 使用应用自身连接日志和指标。
- 在受控调试镜像中安装工具。
- 从宿主机检查 Docker 网络对象。
临时诊断容器示例:
docker run --rm --network order-stack_backend nicolaka/netshoot \
sh -c 'getent hosts mysql && nc -vz mysql 3306 && getent hosts redis && nc -vz redis 6379'生产使用第三方诊断镜像前应先固定版本、扫描来源并遵守安全策略。
22.5 停止与数据验证
docker compose down
docker volume ls --filter name=order-stack
docker compose up -d --wait命名卷仍在,因此 MySQL 和 Redis 数据应继续存在。
不要在有价值数据环境随意执行:
docker compose down -v22.6 从本地Demo演进到生产还缺什么
- 密码改为受控 Secret 系统,不使用普通
.env。 - 镜像由 CI 构建、测试、扫描、签名并推送仓库。
- 部署按不可变 Digest,而不是开发标签。
- MySQL 和 Redis 使用受管服务或有备份、复制、故障恢复的独立架构。
- 日志、指标和链路追踪进入集中平台。
- 配置变更、发布、回滚和权限有审计。
- 多主机高可用、滚动发布和弹性伸缩交给更合适的编排平台。
二十三、常见故障排查
23.1 docker compose up提示变量缺失
docker compose config --environment
docker compose config检查:
- 当前 Shell 是否设置变量。
.env是否位于 Compose 预期目录。- 是否使用了正确
--env-file。 - PowerShell、Bash 的变量设置语法是否混用。
${VAR:?message}是否按设计阻止缺失值。
23.2 修改环境变量后没有生效
不要只执行:
docker compose restart api先检查并重建:
docker compose config
docker compose up -d api
docker inspect --format '{{json .Config.Env}}' <api容器>还要确认 Spring Boot 实际读取的属性名和配置优先级。
23.3 api仍然连不上mysql
按层排查:
flowchart TD
A["api连接mysql失败"] --> B["检查最终连接地址"]
B --> C{"是否错误使用localhost"}
C -- "是" --> D["改用服务名mysql与容器端口3306"]
C -- "否" --> E["检查两个服务是否在同一网络"]
E --> F["检查DNS解析"]
F --> G["检查MySQL健康与监听"]
G --> H["检查账号、密码、库名和权限"]
H --> I["检查连接超时、重试和连接池日志"]命令:
docker compose ps
docker compose config
docker compose logs --tail 200 mysql
docker compose logs --tail 200 api
docker network inspect order-stack_backend23.4 MySQL容器一直Restarting
docker compose ps
docker compose logs --tail 300 mysql
docker inspect --format '{{json .State}}' <mysql容器>
docker inspect --format '{{json .Mounts}}' <mysql容器>常见原因:
- 必填密码变量缺失。
- 数据目录权限不对。
- 数据文件版本与 MySQL 镜像版本不兼容。
- 磁盘已满。
- 配置参数错误。
- 健康检查失败被误认为容器进程退出;两者需要分开看。
23.5 port is already allocated
宿主机端口只能被一个监听者占用:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose ps解决方向:
- 更换宿主机端口,而不是容器内部服务端口。
- 删除真正不再使用的旧容器。
- 数据库只供容器使用时取消
ports。 - 检查是否启动了另一个项目名相同或不同的 Compose 栈。
23.6 出现孤儿容器
从 Compose 文件删除或改名服务后,旧项目容器可能仍存在。先确认项目:
docker compose ls
docker ps --filter label=com.docker.compose.project=order-stack
docker compose up -d --remove-orphans--remove-orphans 会删除不在当前模型中的同项目容器,使用多个 Profile 或覆盖文件时必须确认它不是仍有用途的服务。
23.7 down后数据“丢失”
检查:
docker compose config
docker compose ls
docker volume ls
docker inspect --format '{{json .Mounts}}' <mysql容器>可能原因:
- 使用匿名卷,后来挂到了新卷。
- 项目名改变,创建了新前缀命名卷。
- 执行过
down -v。 - 执行了
volume prune。 - Bind Mount 指向了错误宿主机目录。
- 应用原本写在容器可写层而非 Volume。
23.8 服务状态Healthy但业务仍失败
健康检查只证明测试命令成功,不证明所有业务链路正常。继续检查:
- 健康 endpoint 是否过于简单。
- API 是否能访问数据库、Redis、MQ 和第三方接口。
- 线程池、连接池是否耗尽。
- 业务权限和初始化数据是否正确。
- 请求错误率和 P99,而不只是容器状态。
二十四、Compose适不适合生产
不能只回答“可以”或“不可以”。需要看系统边界。
适合
- 本地开发和集成测试。
- CI 临时依赖环境。
- 单机内部工具。
- 小规模、可接受单机故障的服务。
- 边缘节点或受控设备上的固定应用栈。
不足
- 普通 Compose 主要面向单 Docker Engine,宿主机是单点。
- 缺少完整跨节点调度。
- 没有 Kubernetes Deployment 等价的声明式滚动发布能力。
- 多副本服务发现、负载均衡和故障迁移能力有限。
- Secret、RBAC、策略、审计和多租户能力需要外部系统。
- 有状态服务高可用和存储故障恢复需要单独设计。
选型思路
| 场景 | 更常见选择 |
|---|---|
| 开发机启动依赖 | Docker Compose |
| CI集成测试环境 | Compose或测试容器方案 |
| 单机内部系统 | Compose,加备份、监控和严格运维 |
| Docker原生集群编排 | Swarm,需结合团队现状评估 |
| 多节点微服务、滚动发布、自愈 | Kubernetes或企业容器平台 |
| 云上数据库、Redis | 优先评估受管服务 |
工具不是架构保证。即使使用 Kubernetes,如果没有备份、限流、观测和故障演练,也不能自动得到可靠系统。
二十五、面试标准回答
Docker Compose工作原理
Compose读取一个或多个Compose文件,完成变量插值、配置合并和校验,生成包含services、networks、volumes、secrets等对象的项目模型。随后比较期望模型与当前项目资源,通过Docker Engine API拉取或构建镜像、创建网络和Volume、创建或重建容器,并按依赖关系启动。容器最终仍由Docker daemon、containerd和OCI runtime运行,Compose本身不是新的容器运行时。
docker compose up与start区别
up根据当前配置协调项目,缺少资源时会创建,配置或镜像变化时可能重建容器;start只启动已经存在且停止的容器,不重新按新配置创建。修改环境变量、端口、挂载后只执行start或restart通常不会得到新配置,应先用docker compose config确认,再执行up -d并inspect最终容器。
depends_on为什么不能保证依赖可用
短语法主要保证启动顺序,依赖容器主进程启动不等于数据库已完成初始化。可以为依赖定义healthcheck并使用
condition: service_healthy改善初始启动,但运行中仍可能发生重启、网络抖动和主从切换,所以应用还必须设置连接超时、有限重试、退避、熔断和降级。
.env和env_file区别
项目
.env主要为Compose解析YAML时的${VAR}插值提供值,也可能影响CLI行为;service的env_file用于把变量注入指定容器。一个变量存在于.env不表示它自动进入容器。应先用docker compose config检查解析模型,再用docker inspect检查容器实际Env。
down与down -v区别
down删除项目容器和默认网络,普通命名卷默认保留;down -v还会删除项目声明的相关卷,数据库数据可能因此丢失。Volume只提供独立于容器的生命周期,不等于备份,生产执行前必须核对项目名、最终配置、实际Mounts、目标卷和恢复能力。
Compose为什么不应滥用container_name
Compose原本通过项目名、服务名和实例序号隔离资源,服务名还承担网络DNS名称。固定container_name容易造成跨项目冲突,并阻碍同一服务扩展多个副本。调用方应依赖服务发现名称而不是某个具体实例名或容器IP。
Compose是否适合生产
Compose适合开发、测试、CI临时环境以及可接受单机边界的小规模部署。普通Compose主要协调单个Docker Engine,不提供完整跨节点调度、滚动发布、自愈、多租户和集群存储编排。若系统要求多节点高可用、弹性扩缩和标准化发布,通常使用Kubernetes等平台;即使单机使用Compose,也必须补备份、监控、密钥、回滚和故障恢复。
二十六、学习验收
完成学习后,应能不看答案解释并演示:
- 从两个 Compose 文件推导最终
docker compose config。 - 区分 Compose 插值值和容器最终环境变量。
- 解释项目名变化为什么会创建另一组容器、网络和 Volume。
- 解释服务名 DNS 和容器独立 localhost。
- 说明
up、start、restart、run、exec、down的对象变化。 - 用 healthcheck 与 condition 控制初始顺序,同时说明为什么仍要应用重试。
- 让 MySQL 数据在普通 down/recreate 后仍保留,并证明
down -v的风险。 - 从
config、ps、logs、inspect、network inspect、volume inspect完成故障定位。 - 说明环境变量、Secret 文件和企业密钥系统的安全边界。
- 判断给定系统应使用 Compose 还是集群编排平台,并给出故障域理由。
