Docker Compose
真实项目几乎都是多容器组合:Web 应用 + 数据库 + 缓存 + 反向代理。一个个 docker run 写一长串参数太难维护,而且没法版本化。Docker Compose 用一个 YAML 文件描述所有服务,一条命令管理全部——这是本地开发的神器。
为什么需要 Compose
先看没 Compose 时多痛苦:
# 为什么用 Compose?
# 没有Compose 时,跑一个 web+db+redis 要这样:
docker run -d --name db -v dbdata:/var/lib/postgresql/data \
-e POSTGRES_USER=app -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --name cache redis:7
docker run -d --name web -p 3000:3000 \
-e DATABASE_URL=postgres://app:secret@db:5432/app \
--network mynet myapp
# 问题:
# 1. 命令又长又难记,改个参数要重新敲一遍
# 2. 启动顺序靠人肉保证(先 db 再 web)
# 3. 团队协作没法版本化(同事不知道你跑了啥)
# 4. 一年后回来自己也忘了当时怎么跑的
# 有了 Compose:这些参数全部写进 docker-compose.yml
# 团队共享一个文件,任何人 clone 后:
docker compose up -d
# 完整环境就起来了。版本控制、协作、复现,一步到位Compose 把这些参数全部写进 docker-compose.yml,团队共享、版本控制、一键复现。同事 clone 项目后不用配环境,一条 docker compose up -d 就能跑起来完整服务——这就是它最大的价值。
基础模板
一个典型 Web + DB + 缓存 的 docker-compose.yml 长这样:
# docker-compose.yml:用一个文件描述"多个容器怎么协同"
# 文件名固定 docker-compose.yml(或 .yaml),放在项目根目录
# 版本号(新版 Compose 已不需要,但写上兼容旧版)
version: "3.9"
services:
# 第一个服务:Web 应用
web:
build: . # 从当前目录的 Dockerfile 构建
ports:
- "3000:3000" # 主机端口:容器端口
environment:
- DATABASE_URL=postgres://app:secret@db:5432/app
- REDIS_URL=redis://cache:6379
- NODE_ENV=production
depends_on:
- db # 等 db 启动后再启动 web
- cache
restart: unless-stopped # 崩溃自动重启
volumes:
- ./src:/app/src # 开发时挂载源码,改代码立即生效
# 第二个服务:数据库
db:
image: postgres:16-alpine # 直接用现成镜像
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- dbdata:/var/lib/postgresql/data # 数据持久化(命名卷)
ports:
- "5432:5432"
# 第三个服务:缓存
cache:
image: redis:7-alpine
ports:
- "6379:6379"
# 顶层声明命名卷(被 services 引用)
volumes:
dbdata:三个顶层块要分清:
- services:定义每个容器。这是核心,服务名(web/db/cache)就是DNS 名,容器间能直接用名字互访(
DATABASE_URL=...@db:5432里的db就是服务名)。 - volumes:声明命名卷,实现数据持久化。
- networks:声明自定义网络(本例省略了,Compose 会自动建一个项目级网络)。
depends_on 只控制启动顺序,不等服务"就绪"。比如 db 容器启动了但 MySQL 还在初始化,web 立刻连会失败。要等"就绪"得用 condition: service_healthy 配合 healthcheck(见进阶配置)。
全套命令
# 用 Compose 管理一整套服务
# 一键启动所有服务(-d 后台,首次会自动构建镜像)
docker compose up -d
# 看运行状态(类似 docker ps,但只看本项目)
docker compose ps
# 看所有服务日志(实时跟随)
docker compose logs -f
docker compose logs -f web # 只看 web
docker compose logs --tail 100 web
# 在运行中的服务里执行命令
docker compose exec db psql -U app
docker compose exec web sh
# 重新构建镜像(改了 Dockerfile 后)
docker compose build
docker compose build --no-cache web # 不用缓存构建 web
# 重启某个服务
docker compose restart web
# 停止所有服务(容器还在,可被 start 唤醒)
docker compose stop
# 停止并删除容器、网络(数据卷默认保留)
docker compose down
# 连数据卷一起删(清空数据库数据,慎用!)
docker compose down -v新版命令是 docker compose(空格,集成到主命令,V2 用 Go 重写,更快);老版本是 docker-compose(带横杠,Python 写的独立脚本)。两者功能基本一致,新项目用前者。docker compose down 默认保留数据卷,这是设计如此——避免误删数据库数据;确定要清空加 -v。
进阶配置
真实项目的 docker-compose.yml 通常更复杂,涵盖各种场景:
# docker-compose.yml 进阶配置
services:
web:
build:
context: ./backend # 构建上下文目录
dockerfile: Dockerfile.prod # 指定 Dockerfile
args: # 构建参数(ARG)
BUILD_NUMBER: ${BUILD_NUMBER}
image: myapp:1.0 # 构建后镜像名(可推到仓库)
container_name: myapp-web # 固定容器名
hostname: web # 容器内 hostname
environment:
- NODE_ENV=production
- LOG_LEVEL=$${LOG_LEVEL} # 引用主机环境变量(两个$$转义)
env_file:
- .env # 批量从文件读环境变量
ports:
- "3000:3000"
volumes:
- ./src:/app/src # 绑定挂载
- logs:/app/logs # 命名卷
networks:
- frontend
- backend
depends_on:
db:
condition: service_healthy # 等 db 健康检查通过才启动
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
restart: unless-stopped
deploy: # 仅 Swarm 模式生效
replicas: 3
resources:
limits:
memory: 512M
networks:
frontend:
backend:
internal: true # 内部网络,不能出网
volumes:
logs:
dbdata:
driver: local几个高频技巧:
- 引用主机环境变量:YAML 里
$VAR会被 Compose 用主机环境变量替换。想字面量写美元符号要写两个$$(如 Postgres 里设密码含$)。 - env_file:把一堆环境变量放
.env文件批量读入,比一行行写environment整洁。Compose 还会自动读项目根的.env文件用于变量替换。 - 挂载覆盖容器内目录:"
- /app/node_modules" 这种匿名挂载是技巧——它屏蔽主机对应的 node_modules,让容器用自己的依赖,避免主机(Mac)和容器(Linux)依赖打架。 - resources 限制:在 Compose 单机模式下
deploy.resources不生效,要用老的mem_limit/cpus。Swarm 模式才认deploy。
多环境:override 文件
开发和生产配置通常不同(开发要热重载,生产要压缩)。Compose 支持文件叠加,优雅解决:
# 多环境:用 override 文件覆盖配置
# 默认 Compose 会自动合并两个文件:
# docker-compose.yml 基础配置(生产/通用)
# docker-compose.override.yml 开发覆盖(自动加载,不用 -f 指定)
# docker-compose.yml(基础)
services:
web:
build: .
environment:
- NODE_ENV=production
# docker-compose.override.yml(开发覆盖)
services:
web:
command: npm run dev # 覆盖启动命令为热重载
environment:
- NODE_ENV=development # 覆盖环境变量
volumes:
- ./src:/app/src # 加挂载,改代码即时生效
- /app/node_modules # 防止主机 node_modules 覆盖容器内的
# 显式指定多个文件(任意命名)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
# 区分环境配置:
# docker-compose.yml 基础
# docker-compose.override.yml 本地开发(自动加载)
# docker-compose.prod.yml 生产(显式 -f 指定)
# docker-compose.test.yml CI 测试关键认知:Compose 默认会自动加载 docker-compose.yml + docker-compose.override.yml(后者覆盖前者)。所以约定是:基础文件写通用配置、override 写本地开发覆盖。.gitignore 把 docker-compose.override.yml 排除,每人自定义;生产配置另起 docker-compose.prod.yml,用 -f 显式指定。
实战:3 分钟搭一套 WordPress
把上面学的全用上,体会 Compose 的爽:
# 实战:3 分钟搭一套博客系统(WordPress)
# docker-compose.yml
version: "3.9"
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wordpress
MYSQL_USER: wp
MYSQL_PASSWORD: wppass
volumes:
- dbdata:/var/lib/mysql
wordpress:
image: wordpress:6-php8.2-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wordpress
depends_on:
- db
volumes:
dbdata:
# 一条命令启动整套
docker compose up -d
# 浏览器访问 http://localhost:8080 进入 WordPress 安装向导
# 完事 docker compose down 清理对比一下:不用 Compose,你要敲两条巨长的 docker run、记一堆参数、配网络。现在一个 yml 文件 + 一条命令搞定,而且能版本控制、能复现。这就是 Compose 的魔力。
常见坑
- depends_on 不等于 ready:它只保证启动顺序,不等服务就绪。数据库类服务要加 healthcheck +
condition: service_healthy。 - 变量替换把 yml 写崩:YAML 里
$没转义会被当变量替换。要字面$写$$。 - volume 没声明在顶层:服务里用到的命名卷必须在顶层
volumes:块声明,否则报错。 - 命名冲突:Compose 默认用"目录名_服务名"作为容器、网络前缀(如
myapp_web_1)。不同项目用相同目录名会冲突,可用-p 项目名或COMPOSE_PROJECT_NAME环境变量区分。
Compose 是本地开发不可替代的工具,但生产环境(尤其是多机集群)通常改用 Kubernetes——后者解决"跨主机编排、滚动发布、自动扩缩容"等更复杂的问题。学完 Compose 再看 K8s,概念上会顺畅很多。
← 上一篇 Docker 网络
下一篇 Docker 镜像仓库 →