手工启动两个容器时,我们输入了网络、数据卷、端口、环境变量和启动顺序。Compose 把这些运行意图写进一个 YAML 文件,并按项目统一管理。

compose.yaml 主要包含三类顶层对象:
services:应用的服务,每个服务通常对应一个或多个容器。volumes:需要独立保存的数据。networks:服务之间的通信边界。没有显式声明时,Compose 会创建项目默认网络。现代命令是 docker compose,中间有空格。旧的 docker-compose 是早期独立工具,不应出现在新项目脚本中。
一串 docker run 命令描述的是“依次做哪些动作”;Compose 文件更接近“最终应该有哪些服务,以及每个服务需要什么”。执行 docker compose up 时,Compose 会读取声明、检查当前项目对象,再创建或重建有差异的部分。
这带来三个直接好处:
up,Compose 能重建需要变化的容器,而不是要求人手工对照所有参数。它并不意味着完全幂等。镜像标签可能移动、外部网络会变化、卷中数据会积累,command 也可能执行有副作用的逻辑。Compose 固定的是应用拓扑和运行意图,交付仍需版本、数据和外部依赖管理。
service 是应用层定义,描述镜像、构建、端口、环境、挂载和部署方式;container 是该定义的一次运行实例。默认情况下每个服务创建一个容器,但服务可以被扩展到多个副本。应用之间应该依赖服务名 redis,不应依赖自动生成的容器名。
Compose 用项目名给资源建立归属。例如目录名为 docker-task-api 时,默认网络可能叫 docker-task-api_default,卷可能叫 docker-task-api_task-data。同一份 Compose 文件使用不同项目名启动,可以形成相互隔离的两套应用:
$ docker compose -p task-dev up -d
$ docker compose -p task-demo up -d但两套应用不能同时绑定同一个外部端口,数据卷也因项目前缀而不同。项目名既是隔离边界,也是清理范围;自动化脚本应显式固定它,避免因目录改名而操作错项目。
build 与 image 可以同时存在build.context: . 告诉 Compose 怎样构建;image: docker-task-api:1.0 告诉它给构建结果加什么镜像名。只写 image 通常表示拉取或使用现有镜像,只写 build 则让 Compose 生成项目相关的默认镜像名。课程同时写出二者,是为了让后面的标签和发布命令有明确对象。
ports 与 environment 属于运行配置${APP_PORT:-8080}:8080 左侧经过变量插值决定外部端口,右侧是容器内固定监听端口。REDIS_ADDR: redis:6379 作为环境变量放进 API 容器,不进入镜像层,所以同一镜像可以连接不同环境中的 Redis。
volumes 的两次声明解决不同问题service 下的 volumes 表示“把哪个存储挂到容器哪里”;顶层 volumes 表示“这个项目拥有或引用哪些卷”。task-data:/data 左侧是 Compose 逻辑卷名,右侧是 Redis 容器内路径。
read_only、security_opt、cap_drop、healthcheck 和 restart 不是部署后的补丁。它们决定容器以什么边界运行,应与端口、镜像一样进入版本控制和评审。本课程会在后续章节逐项验证这些设置,而不是只相信 YAML 已经写了。
compose.yaml在 docker-task-api 目录创建:
services:
api:
build:
context: .
image: docker-task-api:1.0
ports:
- "${APP_PORT:-8080}:8080"
environment:
REDIS_ADDR: redis:6379
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"
这里没有给 Redis 配置 ports。API 通过默认网络访问它,外部只能访问 API。
先让 Compose 解析配置,而不启动容器:
$ docker compose config --quiet没有输出并且退出码为 0,表示 YAML 结构和变量插值有效。查看解析后的完整配置可以使用 docker compose config。
config 是非常重要的“编译后视图”。Compose 会合并默认值、解析变量并规范化部分短语法,所以排错时应同时看源文件和解析结果:
$ APP_PORT=18080 docker compose config \
--format yaml | sed -n '/ports:/,/environment:/p'预期能看到外部端口已经成为 18080。这一步不创建容器,适合在执行有状态操作前检查配置。
docker compose config 的完整输出可能包含插值后的环境值。真实项目中如果配置涉及秘密,不要把完整输出随意粘贴到工单、日志或聊天中。
--build 会先构建需要的镜像,-d 让服务在后台运行:
$ docker compose up -d --build
[+] Running 4/4
✔ Network docker-task-api_default Created
✔ Volume docker-task-api_task-data Created
✔ Container docker-task-api-redis-1 Healthy
✔ Container docker-task-api-api-1 Started$ docker compose ps
NAME SERVICE STATUS PORTS
docker-task-api-api-1 api Up (healthy) 0.0.0.0:8080->8080/tcp
docker-task-api-redis-1 redis Up (healthy) 6379/tcpCompose 默认用目录名作为项目名,并把它加到网络、卷和容器名称前面。可以用 -p task-course 指定稳定的项目名。
把启动输出理解成对象依赖顺序:网络和卷先创建,Redis 容器随后创建并通过健康检查,API 才满足 depends_on 条件。这里“4/4”统计的不是四个业务服务,而是 Compose 本次处理的资源与容器动作。
继续用标签观察归属:
$ docker inspect docker-task-api-api-1 \
--format 'project={{index .Config.Labels "com.docker.compose.project"}} service={{index .Config.Labels "com.docker.compose.service"}}'
project=docker-task-api service=apiCompose 会为对象添加项目和服务标签。许多 docker compose 命令正是依靠这些元数据定位项目资源,因此无需把自动生成的容器名写死在业务配置里。
$ curl -s http://localhost:8080/
{"message":"服务已启动,可以创建任务了","service":"docker-task-api"}
$ curl -s -X POST http://localhost:8080/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"完成 Docker 课程"}'
{"title":"完成 Docker 课程","createdAt":"2026-07-15T02:39:53Z"}
$ curl -s http://localhost:8080/tasks
[{"title"创建时间由应用生成,实际值会变化。
现在验证 Compose 管理的是完整应用,而不是一个能返回首页的容器:
$ docker compose exec redis redis-cli LLEN tasks
1
$ docker compose exec api id
uid=100(app) gid=101(app) groups=101(app)第一条从数据层确认任务已保存,第二条确认 API 仍按镜像的非 root 用户运行。功能、数据与运行边界同时通过,验收才完整。
stop、down 与 down --volumes三个命令影响范围不同:
暂停后恢复:
$ docker compose stop
$ docker compose start容器配置变化后,直接再次执行 docker compose up -d。Compose 会只重建需要改变的服务。
restart 为什么不能应用新配置docker compose restart 只是停止并重新启动现有容器,不会因为你改了环境变量或端口就重写创建配置。要让运行配置变化生效,应执行 docker compose up -d,由 Compose 比较配置并重建容器。
down 保留卷是有意设计容器和默认网络是可重建运行对象,命名卷承载持久状态。默认 down 删除前两者、保留后者,正好体现了生命周期分离。--volumes 是明确扩大删除范围的开关,所以应在命令执行前确认数据是否已经备份或确实可丢弃。
如果从 Compose 文件中删除一个 service,旧容器可能成为该项目的 orphan。执行 docker compose up -d --remove-orphans 可以清理不再定义的项目容器,但使用前要确认它们确实属于已删除的服务,而不是临时保留的迁移对象。
配置中的 ${APP_PORT:-8080} 表示:有 APP_PORT 就使用它,否则使用 8080。
$ APP_PORT=18080 docker compose up -d
$ docker compose ps
NAME SERVICE STATUS PORTS
docker-task-api-api-1 api Up (healthy) 0.0.0.0:18080->8080/tcp此时请求地址变成 http://localhost:18080,容器内部仍监听 8080。运行配置发生变化,不需要重建镜像。
检查容器 ID 会发现 API 通常已经被重建,因为端口发布属于创建时配置;Redis 配置没有变化,容器 ID 应保持不变:
$ docker compose ps -q api
<新的 API 容器 ID>
$ docker compose ps -q redis
<原 Redis 容器 ID>这正是声明式收敛的价值:Compose 只替换受影响的服务。若端口没有变化,检查 shell 是否真的为这次命令设置了 APP_PORT,再用 docker compose config 查看最终解析值。
一套可预测的工作顺序是:
compose.yaml 或源代码。docker compose config --quiet 检查结构。docker compose build api。docker compose up -d 收敛运行状态。docker compose ps、健康状态和真实请求验收。docker compose logs 读取问题证据。stop;移除运行对象用 down;只有明确删除数据时才加 --volumes。Compose 非常适合单机开发、教学、测试和一部分单机部署。它不会自动提供跨多台机器调度、滚动更新、集群级自愈等能力。先把单机容器的镜像、网络、卷和健康边界学清楚,再进入集群编排会更稳。
到这里,项目已经从两条手工 docker run 命令变成一份可重复执行的应用定义。下一节会把“容器启动”提升为“服务真的可用”。