
我记得自己刚开始用Docker那会儿最直观的感受是单容器部署太爽了。一个依赖打包进镜像docker run启动端口映射、环境变量、数据卷用参数指定一下搞定。但等服务的数量一上来问题马上就不一样了。后来我转到docker compose整个世界安静下来。这篇笔记是我从零上手 Compose 的记录覆盖了配置文件的常用字段、命令工作流和几个初学时必踩的坑适合刚接触 Docker、想用 Compose 管理多容器的朋友。1. 从一条docker run说起Compose要解决的真实问题1.1 一条命令能部署两条就开始头疼举个很常见的场景。你想在本机跑一个带 Redis 缓存的 Web 应用得敲类似这样的两条命令docker run -d --name redis \ -p 6379:6379 \ -v redis_data:/data \ redis:7 docker run -d --name web \ -p 5000:5000 \ -e REDIS_HOSTredis \ web-demo:latest光看这两条命令问题就藏不住了参数太长换行之后很难一眼读懂下次要改版本号或者换端口得对着历史记录猜。两个容器之间的依赖关系是隐式的。你知道 web 要依赖 redis全靠后面那个环境变量和容器网络撑着别人接手这个环境根本看不出来。换一台机器重新部署就得把这些命令重新敲一遍漏一个参数服务起来就是错的。想停干净要一条一条docker stop、docker rm偶尔还得docker network rm。两个服务已经这样了三个四个呢数据库、消息队列、前端 Nginx、后端应用再加上定时任务命令会变成一长串复制粘贴都嫌长的脚本。我就是在那个阶段开始用 Compose 的认真说它解决的就是多容器怎么定义、怎么一起管理这件事。1.2 Compose到底在解决什么不解决什么Compose 的思想其实很朴素把我要跑哪些容器、它们之间什么关系、端口怎么开、数据挂哪里全部写进一个 YAML 文件然后一条命令完成创建和启动。这份文件是可读的、可提交到代码仓库的、可审查的。新环境拿到它基本就能复现同一套服务。我把这种方式理解成部署即配置之前是命令行临时拼凑现在变成了一份有结构的描述文件人在看文件的时候整个系统的拓扑是一眼就能扫出来的。但这里要给新手朋友提个醒Compose 是标准 Docker 单机能力的组合不负责跨主机的调度和水平扩容。它管的是这台机器上的一组容器不是一群机器上的容器群。后面那个领域属于容器集群那是更后面的学习路线不是这篇笔记的范围。刚入门时你先聚焦在单机多容器的场景里把 Compose 用顺了后续再理解集群调度会轻松很多。2. 第一个Compose项目环境确认、最小配置与启动验证2.1 先确认本机的Compose版本动手之前先看自己环境里的命令是什么形态。现在主流的 Docker 发行版里Compose 已经作为插件内置命令是带空格的docker compose早期独立安装的老版本命令是带连字符的docker-compose。docker compose version如果输出类似Docker Compose version v2.x说明你用的是新语法如果系统提示command not found再试docker-compose version。V1 和 V2 在配置语法上基本兼容但 V2 解析更快、更容易安装我建议优先确保自己在用 V2。这篇笔记里的命令都按新写法来老版本的读者把空格换成连字符即可。2.2 一份最小可用的compose文件Compose 的核心是一个 YAML 文件默认叫docker-compose.yml或者compose.yaml。二者都是合法的我习惯用docker-compose.yml因为老教程、文档、工具链对它最熟悉。先写一份最简单的配置就起一个 Nginx 容器services: web: image: nginx:1.25 ports: - 8080:80再看一眼这个文件它其实只包含一个顶级关键字services下面定义了一个叫web的服务。ports这条映射是最典型的宿主机端口:容器端口把容器里的 80 端口发布到宿主机的 8080 端口。不少老教程会在文件第一行写version: 3.8Compose V2 已经对版本字段做了弱化不写默认按最新规范处理。考虑到它容易让新人误解为版本越高越好或版本号对不上就不行我建议新项目直接省略。旧文件带着版本号也不需要着急改兼容性没有大问题。2.3 启动、验证、停止把流程走一遍在同目录下执行docker compose up -d-d表示 detached让容器在后台运行终端不会一直被日志刷屏。第一次执行会先拉取镜像之后服务创建并启动终端会列出容器的创建结果。看看现在的状态docker compose ps正常会看到web服务对应的容器状态显示Up端口那列有0.0.0.0:8080-80/tcp的映射。这时候你在浏览器或者命令行里访问一下curl http://localhost:8080能收到 Nginx 的默认欢迎页 HTML说明服务已经通了。想实时看它输出什么日志用docker compose logs -f webCtrlC 退出日志跟随不影响容器运行。最后收尾docker compose down这条会把服务对应的容器和默认网络移除但镜像和数据卷都会保留。要理解这条命令和docker stop的区别stop只是暂停容器down是彻底把这个 Compose 项目定义出来的资源回收掉。以后重新up又会基于同一份 compose 文件再创建出来。到这里你已经走完了一个最小的 Compose 生命周期。接下来的问题是真实项目里文件里那些字段到底该怎么填我按使用频率把常用配置整理了一下。3. compose文件的核心配置逐行拆解3.1 服务来源image还是buildservices下每个服务都需要说明镜像从哪来。最简单的是image直接指定远程镜像和标签services: redis: image: redis:7本地没有时会自动从镜像仓库拉取。当你用的是别人构建好的公共镜像时这样最省事。另一种情况你希望用项目里的 Dockerfile 自己构建镜像那就用buildservices: web: build: context: . dockerfile: Dockerfilecontext指定构建上下文目录dockerfile指定文件名。如果文件名就是Dockerfile且就在当前目录可以简写成build: .。context这个概念值得多说一句Docker 构建时可以 COPY 的文件范围必须落在 context 目录内你把 context 指定成项目根目录Dockerfile 里才能 COPY 到所有项目文件。image和build也可以同时出现services: web: build: . image: demo/web:1.0含义是用本地构建出来的镜像同时给它起一个明确的名字和标签这样后续用镜像名引用起来更方便。组合写法在正式项目里很常见因为它既保留了从源码构建的能力又给镜像一个稳定的名字。3.2 端口映射和网络容器内外要分清楚端口这部分新手最容易把容器内端口和宿主机端口搞混。在 Compose 里冒号左边是宿主机端口也就是外部访问用的端口冒号右边是容器内进程监听的端口由镜像里的应用决定。services: web: image: nginx:1.25 ports: - 8080:80nginx 镜像里默认让 Nginx 监听 80所以右边是 80我想在宿主机上用 8080 访问所以左边写 8080。两个端口之间没有必须相等的道理80 映射到任意未占用的宿主机端口都可以。对应的还有一个配置叫expose。它只声明容器会使用某个端口但不把端口发布到宿主机上services: web: expose: - 5000expose的用途主要是在同一个 Compose 网络里让其他服务知道这个端口存在外部要访问还是得靠ports。比如中间件服务的接口只想让内网容器调用不想暴露给宿主机就可以只写expose。关于服务间通信Compose 会为项目自动创建默认网络。只要服务在同一个 Compose 项目里每个服务都能用服务名作为主机名访问对方。比如一个叫redis的容器在容器内监听 6379web 服务的代码里连接地址写redis:6379即可不需要去查 IP。这个特性让配置里的服务发现变得非常简单也是 Compose 编排多服务时的核心便利。3.3 数据卷与环境变量容器一删数据还回不回来容器文件系统是临时的。容器被删除后文件系统里的数据基本就没了。要想持久化必须用volumes把数据挂载到宿主机或者命名卷里。Compose 里有三种常见形态services: redis: image: redis:7 volumes: - redis_data:/data # 命名卷 - ./config:/etc/redis # bind挂载 - /tmp/redis_tmp:/tmp # 直接写宿主机绝对路径 volumes: redis_data:第一种命名卷最推荐Docker 负责管理数据存放位置你只需要给卷起个名字多个服务甚至多个项目都能复用同一个卷。第二种是把宿主机的某个目录绑定到容器里改动宿主机文件容器内立即能看到。开发场景里用它挂代码目录改完代码不用重新构建镜像非常方便。但要注意相对路径是相对于 compose 文件所在目录解析的不是相对你当前命令执行目录。最后一种直接把宿主机绝对路径写死适合临时场景跨机器部署时移植性最差。环境变量这块最常见的两种写法是environment和env_fileservices: web: environment: - REDIS_HOSTredis - APP_DEBUGtrue env_file: - .envenvironment适合写一些需要明确指出、不太敏感也不经常变化的配置env_file则适合把一串变量放到文件里统一管理。有一点要特别提醒如果env_file是那种包含密钥的本地文件比如数据库密码、API Key记得加进.gitignore别提交到仓库。3.4 依赖顺序与重启策略服务挂了怎么办depends_on用来声明服务之间的启动依赖services: web: depends_on: - redis redis: image: redis:7它保证的是启动时Compose 会按依赖顺序启动容器先 redis 后 web。但它不保证 redis 已经完全就绪可能在 redis 还在初始化端口时web 就试着连它了。这是刚入门时最常见的一个认知差依赖了不等于可用了。后面我专门讲怎么用 healthcheck 和 condition 来做严格等待。restart用来设置容器的重启策略很常用services: web: restart: unless-stoppedrestart的常用值和含义大致如下值行为no默认容器退出后不自动重启always无论退出原因总是重启包括守护进程启动时on-failure只有非零退出码时才重启unless-stopped大多数场景推荐除非手动 stop否则总是重启unless-stopped这个值的优点很实在机器重启、或者 Docker 守护进程重启之后服务能自动回来但你手动docker compose stop停掉就不起来。数据库、应用服务这类希望长期在线的容器我一般都会带上这一行。4. 实战编排一个Web服务加Redis的完整项目4.1 项目结构和两个文件先准备好理论知识说完动手跑一个两个服务协作的例子。我假设有一个很简单的访问计数器 Web 服务用户每次访问首页计数加一计数存在 Redis 里。语言和框架不重要我这里用一个 Python 的 Flask 小应用来演示换成 Node、Go、Java 都完全一样编排的思想不变。项目目录结构如下compose-demo/ ├── app.py ├── requirements.txt ├── Dockerfile └── docker-compose.ymlapp.py内容from flask import Flask from redis import Redis app Flask(__name__) r Redis(hostredis, port6379, db0) app.route(/) def index(): count r.incr(visit_count) return fhello, this is visit #{count} if __name__ __main__: app.run(host0.0.0.0, port5000)注意这里 Redis 的地址写的是redis不是localhost也不是某个 IP。这正是利用了 Compose 默认网络的服务发现能力在同一份 compose 文件里服务名redis就是这个主机名。DockerfileFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app.py . CMD [python, app.py]requirements.txt里两行flask redis4.2 编排文件逐段说明docker-compose.yml这样写services: web: build: . ports: - 5000:5000 depends_on: - redis restart: unless-stopped redis: image: redis:7 volumes: - redis_data:/data restart: unless-stopped volumes: redis_data:web 服务用当前目录的 Dockerfile 构建端口映射到 5000启动依赖 redis。redis 服务直接用官方镜像data 目录挂到命名卷redis_data上。如果 redis 要做持久化这个卷就保证容器重来一遍数据还在。启动命令docker compose up -d第一次会先构建 web 镜像过程中能看到 pip 安装依赖的输出。构建完成之后两个容器依次启动。用docker compose ps看看状态正常情况下两条服务都在Up。访问验证curl http://localhost:5000第一次访问返回hello, this is visit #1。再访问一次返回#2。连续几次访问数字一直递增这时能确认两件事一是 web 容器和 redis 容器之间的网络是通的服务名redis在容器网络中可以被正确解析二是 Redis 的计数器状态在正常工作。如果 curl 不通优先用docker compose logs web看应用日志连接 Redis 的报错基本一眼就能定位到。4.3 持久化验证重启、删除容器数据还在吗现在验证数据卷的作用。先把 web 服务重启一下docker compose restart web docker compose ps重启 web 并不会影响 redis计数继续从上次的值往上涨访问之后你会发现 visit #3。接着做更彻底的测试。把 Compose 项目整个关掉再启动docker compose down docker compose up -d curl http://localhost:5000如果你配置了命名卷redis_dataRedis 重启加载了/data目录里的持久化文件访问次数还是接着上一次继续。这里能看到命名卷的价值项目容器的整个生命周期结束数据也没有随着down一起消失。但如果执行的是docker compose down -v-v会连同声明的volumes一起删除那个 Redis 持久化文件也一块被清掉。再访问时计数就重新从 1 开始了。所以down -v要谨慎尤其当卷里存的是生产数据库时这一行命令等于格式化数据。我的习惯是临时测试环境随便用任何带真实数据的卷绝对不碰。4.4 把启动等待做严谨healthcheck的进阶用法前面说了depends_on只保证启动顺序。用在 redis 这个例子里如果 web 容器起来的时候 redis 的 6379 端口还没来得及监听app 进程连 Redis 就会报Connection refused直接崩掉。项目小的时候靠应用层的重试逻辑兜底就够了。更规范的做法是给被依赖的服务配healthcheck再让依赖方等到健康状态services: redis: image: redis:7 healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10 web: build: . ports: - 5000:5000 depends_on: redis: condition: service_healthyhealthcheck让 Docker 定期在容器里执行redis-cli ping返回 PONG 就标记为 healthy。depends_on使用condition: service_healthy后web 容器会等到 redis 被标记为 healthy 才开始创建。这个组合是 Compose 里处理服务真正可用的常用姿势建议新手直接照着抄。5. 日常操作中最常用的Compose命令组合5.1 启动、看日志、进容器一个都不能少Compose 的命令数量不多日常真正高频的其实就那么几个# 构建并后台启动 docker compose up -d # 查看项目内所有容器状态 docker compose ps # 跟随查看某个服务的日志tail限制行数 docker compose logs -f --tail200 web # 在运行中的容器里执行命令 docker compose exec web bash # 跑完即删的一次性命令 docker compose run --rm web pytestlogs是排查问题的第一站。服务启动失败、请求报错、依赖连接失败日志里基本都有线索。加上-f可以像tail -f一样持续跟随配合--tail可以只看最后 N 行不用从头翻屏。exec用来进入容器内做调试比如检查容器里的环境变量、看进程、手动执行命令配合排查。注意它是进到运行中的容器镜像里没有安装 bash 时可以退一步用sh。run和exec长得像但语义不同。run是另外起一个临时容器去执行命令适合做数据库迁移、跑测试、执行一次性脚本配合--rm容器执行完自动清理不污染项目环境。如果你还想看系统的资源占用可以加两个辅助命令docker compose top列出每个服务对应的系统进程docker compose stats按服务显示 CPU 和内存占用。这两个命令在怀疑某个容器吃资源、或者容器内进程异常时比一个个docker inspect来得直观。5.2 down、stop、start、restart生命周期要分清这几个命令在语义上差别很大我见过不少新手把它们用混命令行为数据卷适用场景docker compose stop暂停所有容器进程不删除保留临时停服还打算马上恢复docker compose start启动已存在的容器保留对应 stop 的恢复docker compose restart逐个重启容器保留改了应用配置想快速重启应用docker compose down删除容器和默认网络默认保留卷项目不再需要或想完全重建docker compose down -v连声明的卷一起删删除测试环境重置慎用一点个人经验如果只是临时要停一下用stop就够了别动不动downdown之后再up容器是重新创建的起名、网络、卷引用的细节都要重新走一遍多花了时间还没什么收益。restart看起来方便但要注意它只重启容器不会应用 compose 文件里的新配置也不会重建镜像。后面这点是不少人改完配置不生效的根源。5.3 多文件覆盖和变量一个项目管多种环境同一个服务往往要跑在开发和生产两套配置下。Compose 允许一次指定多个文件叠加docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d后面文件里的内容会覆盖前面文件。base 文件放公共配置覆盖文件放差异化配置比如生产环境开更高版本、挂更多卷、用不同的环境变量值。多文件方式在命令行和 CI 里都很灵活。变量这块用的是.env机制。Compose 启动时会自动读取项目根目录的.env文件里面以KEYVALUE形式写变量compose 文件里用${KEY}引用# .env NGINX_PORT8080services: web: image: nginx:1.25 ports: - ${NGINX_PORT}:80env_file是把变量注入到容器内部环境.env是给 Compose 解析配置文件用的两者服务的对象不同新手很容易混。记住一句话.env管配置文件的变量env_file管容器内的变量。6. 初学阶段最容易踩的五个坑6.1 YAML缩进和格式报错的第一个来源YAML 对缩进极其敏感而且只认空格不认 Tab。新手最常遇到的报错就是在配置里混入了 Tab 或者缩进多了一个空格yaml.scanner.ScannerError: mapping values are not allowed here遇到这类报错第一反应不是改业务配置而是检查缩进结构。我的排查习惯是把services下的每个服务都对齐到同一个层级服务内部的字段统一缩进两个空格数组项用短横线加空格。另外给一个通用建议编辑器装一个 YAML 插件同时给 compose 文件配上语法校验能标出来的错误会比裸编辑器直观很多。6.2 depends_on只是顺序不保证服务就绪这个坑我前面提过两次因为它太典型了。常见场景是web 容器起来直连数据库数据库容器还在初始化连接被拒web 进程直接退出。表面看好像我明明加了 depends_on为什么还连不上。原因就是depends_on只管创建顺序。要真正等待服务可用要么依赖应用层自己做重试要么用healthcheck配合condition: service_healthy。如果你的服务启动脚本里没有重试逻辑加healthcheck就是最简单的方式。这一步在开发初期可以偷懒但服务进入多人协作或接近上线状态时一定要补上。6.3 不挂卷容器一删数据跟着走另外一个高频坑数据库服务没有声明volumes容器一重建数据全没。出问题的现场通常是这样的——数据库跑得好好的某次docker compose down后重新up应用报一堆错数据表全空了因为镜像初始化的空数据已经把旧库顶替了。数据库这类有状态服务数据卷是第一个要配好的内容。哪怕是本地开发我建议也养成声明命名卷的习惯不然哪天顺手down -v或者up --force-recreate就只能望着空日志发呆。6.4 端口冲突宿主机端口被占容器状态却在Up还有一种很迷惑的现象docker compose ps显示服务 Up但浏览器里怎么都访问不了curl 一直失败。原因很可能出在端口映射冲突上宿主机那边预定的端口已经被另一个进程占用了容器可能起不成功或者起了但端口没真正绑定上。排查方式很简单docker ps # 或 ss -tlnp | grep 8080看看 8080 是不是被其他进程占着。是的话把映射改成别的端口就成了。养成习惯不要总把服务都压在常用端口上为项目规划一套不冲突的端口范围能省掉不少麻烦。6.5 改了配置不生效到底要不要重建最后一个普遍困惑我改了docker-compose.yml或者改了代码为什么docker restart之后看到的行为还是老的关键点在于docker compose restart只是重启现有容器它不重建镜像也不根据新配置生成新容器。改了 Dockerfile 之后需要的是--build参数触发重新构建改了 compose 文件里的端口、卷、环境变量需要的是重新up让 Compose 对比差异并重建受影响服务# 改了代码/Dockerfile docker compose up -d --build # 改了端口、卷、环境变量等配置 docker compose up -d理解了这套逻辑很多改半天没效果的问题就都能解释通了。配置文件不是热更新机制该重建的时候别用 restart 糊弄该 restart 的时候也别动不动 down。学到这里Compose 的核心使用路径已经完整走了一遍从单条docker run演进到一份 compose 文件把一个包含两个服务的小项目从配置、启动、验证到持久化都跑通又理清了日常命令和多环境覆盖。我自己带新人时的经验是两个固定动作第一每写一个不熟悉的配置就先跑一遍docker compose config它会把最终的配置解析结果打印出来比对着文档猜字段靠谱得多第二把一份描述文件、一条命令启动作为默认工作方式记在脑子里哪怕只是临时起一个 Redis用 Compose 写也比反复敲docker run值得。下一步你可以试着把项目里已有的容器服务迁到 Compose 里先从单服务开始再逐步加依赖。这个过程里踩过的坑基本就是下篇笔记要写的内容了。