Docker Compose 实战：从多容器编排到生产级最佳实践
`docker run` 一次只能起一个容器，而真实的应用几乎都是「多容器协作」：前端、后端、数据库、缓存、消息队列……靠一条条 `docker run` 命令启动，命令会越来越长，忘记参数、端口冲突、启动顺序错乱是家常便饭。Docker Compose 就是来解决这个问题的：用**一个 YAML 文件**声明整组服务，一条命令启动、停止、重建整个应用栈。本文从零到实战，讲清 Compose 的核心概念、完整案例和生产级最佳实践。## 安装与版本：认准 Compose V2**Compose V1（`docker-compose`，带横杠的命令）已于 2023 年 7 月停止更新**，请一律使用 V2：```bash # V2 正确用法：docker compose（空格分隔） docker compose version# 未安装时，Docker Desktop / Docker Engine 插件方式 # Linux 上安装插件: # sudo apt install docker-compose-plugin ```V2 用 Go 语言重写，性能更好、原生支持 BuildKit，容器命名自动带项目前缀（如 `myapp-web-1`）。**V1 时代的 `version` 字段已弃用**，新写的 compose 文件直接省略即可：```yaml # 正确：无需 version 字段 services: web: image: nginx:alpine ```配置文件默认叫 `compose.yaml` 或 `compose.yml`（兼容旧名 `docker-compose.yml`）。## 核心配置逐项拆解一个 compose 文件的骨架只有三块：`services`（必需）、`volumes`、`networks`。```yaml services: web: image: nginx:alpine # 直接用镜像 app: build: ./app # 或从 Dockerfile 构建 volumes: db_data: # 命名卷 networks: app_net: # 自定义网络 ```### image 与 build- `image`: 拉取现成镜像 - `build`: 指定构建上下文（`context`），可配合 `dockerfile` 和 `target` 做多阶段构建 - 两者可同时写：先构建，再用 `image` 给构建结果命名### ports 与 expose```yaml ports: - "8080:80" # 宿主机8080 → 容器80 - "127.0.0.1:3306:3306" # 仅本机可访问(数据库标配) - "8000-8010:8000-8010" # 端口范围 expose: - "3306" # 仅容器间可见, 不映射到宿主机 ```**生产原则：数据库、Redis 等内部服务的端口只绑定 `127.0.0.1`，或干脆不映射**——容器间通过服务名直连，根本不需要宿主端口。### volumes：三种挂载方式```yaml volumes: - db_data:/var/lib/mysql # 命名卷: Docker 管理, 数据持久化, 数据库首选 - ./src:/app # 绑定挂载: 开发热更新神器 - type: tmpfs target: /tmp # 内存盘: 临时文件 ```### environment 与 env_file```yaml environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?请设置数据库密码} # 必填校验 - DEBUG=${DEBUG:-false} # 默认值 env_file: - .env ```敏感信息**永远不要硬编码**进 compose 文件并提交 Git——用 `.env` 文件（加入 `.gitignore`）或 secrets 管理。### depends_on 与 healthcheck：正确的启动顺序`depends_on` 只保证**启动顺序**，不保证**服务就绪**——MySQL 容器起来了不代表能接受连接。正确姿势是配合健康检查：```yaml services: db: image: mysql:8.0 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 start_period: 30s # 启动宽限期, 初始化慢的镜像要给够 app: build: ./app depends_on: db: condition: service_healthy # 真正等 db 就绪 ```### restart 与资源限制```yaml services: app: restart: unless-stopped # 生产推荐: 崩溃自动拉起, 手动停止不拉起 deploy: resources: limits: cpus: "1.0" # 单容器最多 1 核 memory: 512M # 最多 512MB ```### logging：防止日志撑爆磁盘```yaml services: app: logging: driver: json-file options: max-size: "10m" # 单个日志文件最大 10MB max-file: "3" # 最多保留 3 个 ```## 实战案例：Web + MySQL + Redis 完整应用栈以最常见的「后端 API + MySQL + Redis」为例，一份文件管起整个应用：```yaml services: api: build: ./api ports: - "8080:8080" environment: DB_HOST: mysql # 服务名即 DNS, 自动解析 DB_NAME: myapp DB_USER: myapp DB_PASSWORD: ${DB_PASSWORD:?必须设置 DB_PASSWORD} REDIS_HOST: redis depends_on: mysql: condition: service_healthy redis: condition: service_healthy restart: unless-stopped deploy: resources: limits: memory: 512M logging: driver: json-file options: max-size: "10m" max-file: "3"mysql: image: mysql:8.0 environment: MYSQL_DATABASE: myapp MYSQL_USER: myapp MYSQL_PASSWORD: ${DB_PASSWORD:?必须设置 DB_PASSWORD} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?必须设置 MYSQL_ROOT_PASSWORD} volumes: - mysql_data:/var/lib/mysql # 命名卷持久化 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 start_period: 30s restart: unless-stopped # 不映射端口到宿主机, 仅容器网络内可访问redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 restart: unless-stoppedvolumes: mysql_data: redis_data: ```启动与日常操作：```bash # 首次启动: 构建 + 后台运行 docker compose up -d --build# 查看状态和日志 docker compose ps docker compose logs -f api# 进容器调试 docker compose exec api bash# 验证配置是否正确(不启动) docker compose config# 停止并清理(容器+网络; -v 会连数据卷一起删, 慎用) docker compose down# 更新代码后重建 docker compose up -d --build ```启动后，`api` 容器里直接 `mysql:3306`、`redis:6379` 就能连上数据库和缓存——这就是 Compose 自动网络的功劳（同一项目内服务名即 DNS，详见之前的 [Docker 网络模式详解](https://blog.astarry.top/docker-network-modes-guide/)）。## 常用命令速查| 命令 | 用途 | | --- | --- | | `docker compose up -d` | 后台启动全部服务 | | `docker compose up -d --build` | 重新构建并启动 | | `docker compose down` | 停止并删除容器/网络 | | `docker compose down -v` | 同上并删除数据卷（**数据会丢，慎用**） | | `docker compose ps` | 查看服务状态 | | `docker compose logs -f [服务名]` | 实时查看日志 | | `docker compose exec [服务名] bash` | 进入容器执行命令 | | `docker compose run --rm api pytest` | 运行一次性任务 | | `docker compose config` | 校验并查看合并后的配置 | | `docker compose pull` | 拉取所有服务的最新镜像 | | `docker compose up --scale web=3` | 扩容（指定了 container_name 或固定端口时不可用） | | `docker compose watch` | 开发模式热重载 |## 高级技巧### Profiles：一份文件管多套环境把可选服务（压测、监控）打上 profile 标签，默认不启动，需要时显式拉起：```yaml services: app: image: myapp:latest grafana: image: grafana/grafana profiles: ["monitoring"] # 默认不启动 ``````bash docker compose --profile monitoring up -d # app + grafana docker compose up -d # 只有 app ```### 多文件合并：开发/生产配置分离```yaml # compose.yaml 基础配置 # compose.prod.yaml 生产覆盖(资源限制/日志轮转等) ``````bash docker compose -f compose.yaml -f compose.prod.yaml up -d ```开发环境绑定挂载源码（热更新），生产环境用镜像内置代码——兼顾开发效率与运行安全。### Watch 模式：开发热重载（V2.22+）```yaml services: app: image: node:20 develop: watch: - path: ./src action: sync # 同步文件不重启(配合框架热加载) - path: ./package.json action: rebuild # 依赖变化才重建镜像 ``````bash docker compose watch ```### Secrets：敏感信息更安全的存放方式官方镜像支持 `*_FILE` 后缀环境变量从文件读密码（如 `MYSQL_ROOT_PASSWORD_FILE`），配合 compose 的 secrets 挂载到 `/run/secrets/`：```yaml services: mysql: image: mysql:8.0 secrets: - mysql_root_password environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_passwordsecrets: mysql_root_password: file: ./secrets/mysql_root_password.txt # 该文件不入 Git ```## 最佳实践清单1. **启动顺序用 healthcheck + `condition: service_healthy`**，别裸用 `depends_on`；`start_period` 要给够（MySQL 建议 30s+） 2. **敏感信息进 `.env` 或 secrets**，`.env` 加 `.gitignore`，提供 `.env.example` 模板，用 `${VAR:?error}` 快速失败 3. **生产环境 `restart: unless-stopped`**（区别于 `always`：手动 stop 后不会自动拉起） 4. **数据库不映射宿主端口**（或只绑 127.0.0.1），容器间走服务名 5. **配日志轮转（max-size/max-file）**，否则日志能撑爆磁盘 6. **配资源限制（deploy.resources）**，防止单个容器吃光主机 7. **复杂项目拆网络**（frontend/backend 分网），数据库只放后端网络，最小权限原则 8. 定期 `docker system prune -f` 清理悬空镜像与构建缓存## 常见坑**坑 1：app 连不上数据库，报 connection refused。** 十有八九是 `depends_on` 没配 healthcheck——MySQL 容器起来了但还没初始化完。按上文配 `condition: service_healthy` 解决。**坑 2：端口冲突起不来。** 服务间改用服务名通信（桥接网络），宿主机端口冲突时改映射即可，`docker compose ps` 能看到具体报错。**坑 3：`down -v` 把数据库数据删了。** 卷是持久化的命根子，删除前确认；备份用命名卷的导出（`docker run --rm -v :/data -v $(pwd):/backup alpine tar czf /backup/db.tar.gz /data`）。**坑 4：`.env` 改了没生效。** 环境变量在 `up` 时注入，改完 `.env` 需要重新 `docker compose up -d`（V2 会重新读取；但已运行容器需 recreate）。**坑 5：日志把磁盘写满。** 没配 `logging` 限制的容器日志无限增长，`du -sh /var/lib/docker/containers` 能确认，配好 max-size/max-file 一劳永逸。## 总结Compose 的价值在于：**把「一组容器怎么跑」从记忆和命令历史变成一份可版本化、可分享的配置文件**。对中小项目、开发测试环境和流量不大的生产服务，一份写好的 compose 文件配合 CI/CD 完全够用，不必事事上 Kubernetes。记住核心原则：开发用绑定挂载、生产用镜像内置；敏感信息走 `.env`/secrets；`restart: unless-stopped` + healthcheck + 日志轮转 + 资源限制，这四件套配齐，你的 compose 应用栈就稳了。## 参考资料- [Docker 官方文档：Defining and running multi-container applications with Docker Compose](https://docs.docker.com/guides/docker-compose/) - [Docker Compose 多服务编排实战：从开发环境到生产部署](https://cn.hostease.com/blog/tutorials/docker-compose-multi-service-orchestration/) - [Docker Compose 实战指南（阿里云开发者社区）](https://developer.aliyun.com/article/1728594) - [Docker Compose 部署全攻略：从入门到精通](https://developer.baidu.com/article/detail.html?id=7122870)