
1. 为什么一定要用一个编排工具来管理容器单机用 Docker 部署一两个容器时docker run敲起来还算顺手。可一旦应用变成“数据库 缓存 后端接口 前端页面 网关”这种多组件形态手敲命令就会变得非常痛苦。我见过不少团队在演示环境部署一套应用要打开好几个终端窗口每条命令一串 IP、端口、环境变量漏掉一个参数整个服务就起不来。Docker Compose解决的就是这个“多容器应用如何快速、稳定、可重复地启动”的问题。它把原本需要逐条执行的docker run参数全部写进一个docker-compose.yml文件里然后用一条命令把整个应用栈拉起来。这个思路和脚本化部署很像但它比脚本更进一步文件本身就是一份可读性极高的架构文档新人拿到后不需要读一大段 shell 脚本直接看 YAML 就能知道系统里有哪些服务它们之间怎么通信。这篇内容围绕标题里的“快速部署”展开适合这几类人看已经会用docker run跑单个容器但面对多容器编排时心里没底的开发者需要在一台机器上快速搭起来整套环境做联调、演示、验收的测试工程师觉得手动维护一堆启动命令太麻烦想用更规范的方式管理项目依赖环境的运维或全栈开发。说白了 Compose 不是用来管理几百个容器的集群工具它更贴近“单机多容器应用”的快速编排Kubernetes 管不了也懒得管的场景它反而做得很顺手。这一篇我不打算只是列命令而是把从“为什么要这样做”到“踩过哪些坑”整个过程讲清楚。2. Compose 的核心概念认真读懂一份 yml 文件就够了2.1 先搞清楚一份 compose 文件的长相docker-compose.yml用的是 YAML 语法。YAML 以缩进表示层级关系这一点和 Python 类似对缩进敏感Tab 和空格混用会直接报错。一个最基础的文件长这样services: web: image: nginx:1.26 ports: - 8080:80这个文件声明了一个叫web的服务使用nginx:1.26镜像把宿主机的 8080 端口映射到容器内的 80 端口。保存为docker-compose.yml后在同一个目录下执行docker compose up -dCompose 会拉取镜像、创建容器并在后台启动服务。你不用记住docker run -d -p 8080:80 nginx:1.26这条命令的任何参数参数都写进 YAML 里了。2.2 services 是一等公民一个服务就是一个容器在 Compose 的世界里项目由服务组成服务名是你在 Compose 文件里自定义的。上面的web就是一个服务名。服务名不仅仅是标识它在 Compose 创建的自定义网络中充当主机名也就是说同一个网络里的其他容器可以直接用http://web访问它完全不用关心容器的 IP 地址。这一点非常关键后面排查容器互连问题时会经常用到。每个服务常用的配置项包括配置项作用说明备注image指定镜像名和标签建议固定版本少用latestbuild指定 Dockerfile 路径构建自定义镜像与image二选一或搭配使用container_name自定义容器名不指定则默认为“项目名服务名序号”command覆盖镜像默认启动命令注意优先级高于 Dockerfile 里的 CMDenvironment注入环境变量可用列表格式或键值映射格式ports宿主机端口与容器端口映射注意容器间通信不需要做端口映射volumes挂载宿主机目录或命名卷数据持久化的关键depends_on声明服务间的启动依赖不等于等待服务就绪要配合健康检查restart设置重启策略no、always、on-failure、unless-stopped理解了services这个核心概念后整个 Compose 文件的骨架基本就定了。后面所有的配置都是围绕“每个服务怎么定义”来展开的。2.3 зависимость 与健康检查别让依赖变成摆设很多新手在depends_on上踩过坑写了它但是依赖服务并没有真正就绪照样报连接失败。原因在于depends_on只控制启动顺序不判断服务是否已经可以对外提供服务。比如数据库容器进程起来了但 MySQL 内部的初始化还没有完成此时后端服务去连数据库依然会失败。在 Compose 规范中推荐使用长格式语法配合健康检查来真正解决依赖就绪问题services: db: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: rootpass healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1] interval: 5s timeout: 3s retries: 10 api: build: ./api depends_on: db: condition: service_healthy这样 Compose 会等数据库健康检查通过后才启动api。健康检查的本质是容器内定期执行某个命令返回成功才认为健康。这是多容器编排里最值得养成好习惯的配置没有之一。把依赖关系从“我觉得数据库应该好了”变成“系统确认数据库能连了”整个栈的稳定性会上升一个台阶。3. 一份可复用的部署文件是怎么写出来的3.1 动手之前先列一张“部署清单”我在实际项目中养成了一个习惯不急着写 YAML先在纸上把整个应用拆成组件清单。以一套常见业务系统为例它通常包含组件镜像端口映射持久化数据环境变量网关入口nginx:1.2680:80配置文件挂载无后端 APInode:20 构建无仅容器内网无DATABASE_URL、REDIS_URLMySQLmysql:8.43306:3306mysql_data 卷MYSQL_ROOT_PASSWORDRedisredis:7.46379:6379redis_data 卷无定时任务基于 api 镜像无无无通过这个清单能很清楚地看出两个关键结论第一只有面向外部访问的组件才需要映射宿主机端口服务之间通过 Compose 内部网络通信不需要也不应该走宿主机端口。第二有状态的数据数据库、缓存必须挂在卷上否则容器一删数据就没了。3.2 组织项目目录与第一版 compose 文件项目目录通常放在仓库根目录下和代码一起管理. ├── docker-compose.yml ├── .env ├── nginx/ │ └── nginx.conf ├── api/ │ ├── Dockerfile │ └── src/ └── docs/第一版 compose 文件我会尽量精简先保证“能跑起来”再逐步加健康检查、日志、重启策略等优化项services: db: image: mysql:8.4 container_name: app-db environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -p${MYSQL_ROOT_PASSWORD}] interval: 5s timeout: 5s retries: 20 redis: image: redis:7.4 container_name: app-redis volumes: - redis_data:/data healthcheck: test: [CMD-SHELL, redis-cli ping | grep PONG] interval: 5s timeout: 3s retries: 10 api: build: ./api container_name: app-api environment: DATABASE_URL: mysql://root:${MYSQL_ROOT_PASSWORD}db:3306/${MYSQL_DATABASE} REDIS_URL: redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_healthy web: image: nginx:1.26 container_name: app-nginx ports: - 80:80 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - api volumes: mysql_data: redis_data:这里的db和redis在别的服务里被直接以服务名访问db:3306、redis://redis:6379这就是 Compose 内部网络最直观的体验。你不需要关心 MySQL 的宿主机 IP 是多少也不用查 Redis 容器的具体 IP服务名就是网络里的主机名。3.3 用一条命令完成整个部署文件写好后执行docker compose up -dup命令会自动完成创建自定义网络默认以项目名命名→ 按依赖顺序创建容器 → 启动服务。-d表示后台运行不加的话日志会铺满当前终端。启动过程中如果发现镜像不存在会自动拉取。如果你的服务需要重新构建镜像比如api目录下的代码改了用docker compose up -d --build验证部署状态时docker compose ps会列出所有服务、状态、端口映射信息。日志则用docker compose logs -f api-f会持续跟踪日志输出调试阶段几乎离不开这个命令。4. 学会这组命令日常维护就不用慌4.1 新老命令的差异要分清楚这里有一件会困扰不少人的事情网上充斥着两种写法一种是带横杠的docker-compose一种是带空格的docker compose。新版 Docker 已经内置了 Compose 插件命令格式是docker compose中间有空格。老版本则是一个单独的二进制docker-compose。Docker 官方从 Compose v2 开始推荐使用新格式。如果你的环境里docker compose version能正常输出版本信息那就用新格式如果提示命令不存在而docker-compose可用多半是用了老版本插件可以装一下 Docker 插件或者沿用旧命令功能上没有太大区别。需要注意docker-compose.yml和compose.yaml这两个文件名都能被识别但项目里最好统一。我在仓库里固定使用docker-compose.yml因为大家对 Vim 补全和文档搜索的惯性认知更容易认这个文件。4.2 生命周期管理up、down、start、stop 的边界最常用的几个命令如下docker compose up -d启动整个项目容器不存在则创建。docker compose down停止并删除容器、网络但默认不删除卷数据仍在。docker compose down -v停止并删除容器、网络、卷数据会彻底删除。这条命令请极其谨慎使用。docker compose stop停止容器但不删除容器和网络。docker compose start启动已停止的容器。docker compose restart重启服务相当于 stop start。docker compose exec api sh进入某个容器执行命令适合排查容器内部状态。生产环境的标准更新姿势是改了代码和 YAML 后直接执行docker compose up -d --buildCompose 会对比配置重建发生变更的服务未变更的服务保持不动。这种方式比“先 down 再 up”安全得多能减少不必要的容器重启。4.3 环境变量的优先级是一个绕不开的知识点Compose 里至少有四种方式传递变量Shell 环境变量优先级最高.env文件配置文件里的environment字段env_file指定的文件优先级最低注意这里environment注入的是“容器内环境变量”对宿主机没有影响.env主要用于 Compose 自身做变量替换比如${MYSQL_ROOT_PASSWORD}就是读取.env里的值。我常用的方式是把敏感信息放进.env且明确写入.gitignore不提交到代码仓库。而nginx.conf这些配置文件则是明确提交的这样新同事克隆仓库后只需要复制一份.env.example为.env改一改密码执行docker compose up -d就能跑起来整套环境。5. 我踩过的那些坑和对应的排查套路5.1 端口冲突导致服务起不来最常见的报错是port is already allocated。原因很简单宿主机的 80 或 3306 端口已经被其他进程占用了。排查顺序建议从简到繁# 看端口被哪个进程占用 lsof -i :80 # 或者用 ss 也可以 ss -tlnp | grep :80如果宿主机上已经有一个 MySQL 在跑你再映射一个 3306 出来必然冲突。解决方式不是去杀进程而应该把 compose 里容器的宿主机端口改一个比如3307:3306。注意容器内端口永远保持默认因为容器之间通过内部网络访问根本不走映射端口。还有一种容易忽略的情况不同 Compose 项目之间也可能端口冲突。如果两个项目都映射宿主机的 8080它们自然就会打架。规划端口时尽量避开常见端口段或者在.env里把端口做成变量。5.2 容器之间连不上IP 是动态的服务名才是静态的我见过有人在代码里写localhost:3306连接数据库容器模式下这是必现失败。容器里的localhost是容器自身的回环地址不是宿主机更不是另一台容器。容器 A 访问容器 B 的正确姿势是使用服务名比如连接数据库应该写成mysql://root:passdb:3306/app。原因解释一下Compose 默认会为项目创建一个桥接网络并启用 DNS 解析。服务名在这个网络里是唯一主机名容器启动后会自动注册进去其他容器按名字解析就能得到对应 IP。而每次容器重建 IP 都可能变化所以不要依赖写死的容器 IP。如果还是连不上优先检查两个容器是不是真的在同一个网络里docker compose exec api cat /etc/resolv.conf # 或者看容器网络详情 docker inspect app-api | grep -i network还有一种情况是网络隔离如果你自己手动创建了额外网络而服务没有加入 Compose 的默认网络那服务名解析自然就失效了。少做手工网络操作所有网络都交给 Compose 管理是避免这类问题的最优解。5.3 修改了配置但看不到变化镜像缓存与不更新容器的双重陷阱这是一个极高频问题。绕不开的原因可能有两个第一代码改了但镜像没重建。docker compose up -d如果检测到镜像没有变化不会重新构建。改完代码记得用--build参数强制重建镜像。第二配置文件改了但容器没重建。Compose 在判断是否需要重建容器时只比对容器配置和当前 YAML 是否一致某些挂载文件比如 nginx 配置因为内容嵌入卷里并不总能触发容器重建。这时候用docker compose up -d --force-recreate web强制重建web这个服务让挂载的配置重新生效。还有镜像 tag 的坑不要在生产环境用image: nginx:latest因为latest指向的镜像内容随着每次拉取可能不一样而本机如果已经拉取过latestCompose 不会自动重新拉取。收敛的做法是固定精确版本号比如nginx:1.26.3并且在需要升级时手工修改版本号再up -d。5.4 容器删了数据没了卷的归属关系要说清楚容器适合做无状态处理有状态的数据必须交给卷。MySQL 的/var/lib/mysql、Redis 的/data都算有状态目录。把这两个目录挂到命名卷后即使执行docker compose down再up数据依然在。排查数据丢失问题时可以先确认卷还在不在docker volume ls | grep postgres docker volume inspect 卷名很多“数据丢了”的现场其实是这么来的第一次用down时数据还在之后有人动了down -v卷被一并删除了。所以团队协作时我建议把down -v、rm -f $(docker ps -aq)这类命令在文档里标红明确只允许在彻底清理环境时使用。5.5 启动顺序正确但服务依然报错健康检查才是正解前面提到过depends_on的局限。举个真实例子有一次我部署一个依赖 MySQL 的服务depends_on写了db但是后端启动仍然报Access denied for user。查了半天才发现 MySQL 容器虽然起来了但初始化 root 密码和建库的操作还没执行完后端服务抢跑了。后来把所有依赖服务的depends_on都改成条件模式加配healthcheck这个问题才彻底解决。这也验证了一句话顺序正确不等于就绪就绪才等于可以连接。6. 把 Compose 用出效率的几个长期习惯6.1 用配置文件代替记忆用目录代替零散命令Compose 最大的价值不只是“一键部署”而是把部署知识沉淀在配置文件里。新人入组后不用翻几十页文档看一个docker-compose.yml就能把整个应用的拓扑结构讲清楚。多服务之间如何连接、依赖哪个数据库、挂载哪个目录、暴露哪个端口全都在文件里信息密度远超口头传承。我在实际项目里的做法是每个独立应用一个独立目录目录内只放应用相关的 compose 文件和配置文件不和其他项目混在一起。docker compose -f 指定文件路径也能用但能把文件放在应用目录下就能省掉-f参数减少输入出错概率。6.2 给关键服务都加上健康检查几乎每个服务都可以配一个healthcheck成本极低收益极高。一个最简单的检查方式services: api: build: ./api healthcheck: test: [CMD-SHELL, curl -f http://localhost:8080/health || exit 1] interval: 10s timeout: 5s retries: 3加上健康检查之后docker compose ps里能看到healthy状态依赖项也能用condition: service_healthy做真正的等待。没有健康检查的 Compose 配置服务只能算“数量上起来了”不能说“可用”。换一个思路理解健康检查让部署从“我认为好了”变成“它自己确认好了”。6.3 日志、时区、资源限制这些细节也不要省生产部署经验丰富的人看一份 compose 文件常常会关注这些点logging限制日志文件大小防止容器日志把磁盘写满容器内时区通常默认 UTC国内应用建议挂载宿主机/etc/localtime或在环境变量里设置TZAsia/ShanghaiCPU 和内存限制用deploy.resources.limits配置防止某个服务异常占用把整台机器拖垮。这些字段不会被docker run的常用参数直接覆盖但缺失时才会意识到它们的价值。比如不配日志轮转MySQL 在写入多的场景下几天就能攒出几十 GB 日志。把配置补上比事后去清理日志轻松得多。6.4 多环境的统一入口.env控制一切差异同一套 compose 文件通过不同的.env就能走不同环境。开发库、测试库、生产库的差异我认为最好体现为.env里的几个变量值不同而不是维护多份 YAML。记住一条原则Compose 文件越晚保持不变越好。环境之间的差异收敛到环境变量配置继续留在 YAML 里既能保证行为一致又能降低复制粘贴多份 YAML 带来的维护成本。我自己的习惯是维护一个.env.example把所有用到的变量写清楚并给出默认值。新环境部署时复制它再修改实际值即可既不会漏变量也不会把真实密码带到仓库里。7. 最后分享一点个人体会如果你之前习惯用一堆 shell 脚本维护容器启动不妨花一两个小时把自己手头最常用的那套部署流程改成 Compose。改完之后你大概率会有一种感觉原来那些“容器环境不稳定”“环境老是跑不起来”的问题很多并不是 Docker 的锅而是部署过程缺少一个可读、可维护、可复用的描述文件。动手时从最小配置开始先把服务跑通再加依赖检查、健康检查、日志限制、资源限制。这套循序渐进的路子对新手很友好也适用于老项目改造。我在无数个“起不来”的现场里排查到最后几乎都能落到卷没挂、端口冲突、依赖没等就绪这三个老问题上。如果这篇文章能帮你避开其中任何一个我在踩坑路上花的时间就值了。