Docker Compose是Docker生态中最贴近日常开发工作流的一个工具。很多刚接触容器化开发的朋友,经常遇到这样一个困扰:项目里需要跑一个Web服务、一个数据库、一个缓存、一个消息队列,每次启动都要敲一堆docker run命令,端口映射、网络配置、数据卷挂载,一个个手打下来又累又容易出错。Docker Compose就是为了解决这个痛点而生的——它让你用一个YAML文件把整个应用的全部容器、网络、存储都描述清楚,然后一条命令就能把整个系统拉起来。

这篇文章会从最基础的开发环境搭建开始,一路走到生产环境的架构优化,把Docker Compose实战中真正会用到的技巧全部展开讲清楚。

一、Docker Compose到底解决什么问题

单个Docker命令能帮你跑一个容器,但当项目里需要多个容器协作时,问题就来了。比如你的项目需要一个Node.js后端、一个MySQL数据库、一个Redis缓存,用docker run启动的时候,你得手动指定网络、端口、环境变量、数据卷,然后记住哪些容器之间需要通信。稍微复杂一点的项目,光命令行就要敲二十行,而且每次改个配置还得重新跑一遍。

Docker Compose的核心思想是"声明式配置"——你把应用需要的所有东西写在一个YAML文件里,然后执行docker compose up,Compose会自动帮你把该建的建好,该连的连上,该挂载的挂好。

二、开发环境快速搭建

2.1 项目目录结构

以一个典型的前后端分离项目为例,目录结构通常长这样:

my-app/
├── frontend/
│   ├── Dockerfile
│   └── package.json
├── backend/
│   ├── Dockerfile
│   ├── src/
│   └── package.json
├── mysql/
│   └── init.sql
├── docker-compose.yml
└── .env

这个结构把每个服务单独放在一个文件夹里,每个文件夹里有自己的Dockerfile和源码,docker-compose.yml放在项目根目录,统一编排所有服务。

2.2 docker-compose.yml核心结构

一个docker-compose.yml文件由三个顶层关键字组成:services(定义容器服务)、networks(定义网络)、volumes(定义数据卷)。下面是一个完整的前后端项目编排示例:

【技术栈:Docker Compose】

# docker-compose.yml - 完整前后端分离项目编排配置
# 技术栈:Docker Compose
# 说明:本配置包含前端、后端、数据库、缓存四个服务

version: "3.9"  # Compose文件格式版本,建议使用3.9以获得最新特性

services:
  # ======================== 前端服务 ========================
  frontend:
    build:
      context: ./frontend          # 构建上下文指向frontend文件夹
      dockerfile: Dockerfile       # 使用frontend文件夹下的Dockerfile
    ports:
      - "3000:80"                 # 宿主机3000端口映射到容器内80端口
    environment:
      - NODE_ENV=development       # 指定前端运行环境为开发模式
      - BACKEND_URL=http://backend:3001  # 前端请求后端的地址
    depends_on:                   # 声明启动顺序依赖(不影响健康检查)
      - backend
    networks:
      - app-network               # 加入自定义网络app-network
    restart: on-failure           # 异常退出后自动重启
    # 开发模式下挂载源码,实现热更新
    volumes:
      - ./frontend:/app           # 将宿主机前端目录挂载到容器/app
      - /app/node_modules         # 排除node_modules,避免覆盖容器内的依赖

  # ======================== 后端服务 ========================
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "3001:3001"               # 后端API对外暴露3001端口
    environment:
      - NODE_ENV=development
      - DB_HOST=mysql             # 数据库主机名(Compose内部DNS解析)
      - DB_PORT=3306
      - DB_USER=${DB_USER}        # 从.env文件读取数据库用户名
      - DB_PASSWORD=${DB_PASSWORD} # 从.env文件读取数据库密码
      - DB_NAME=${DB_NAME}        # 从.env文件读取数据库名
      - REDIS_HOST=redis          # Redis主机名
      - REDIS_PORT=6379
    depends_on:                   # 后端依赖数据库和Redis先启动
      mysql:
        condition: service_healthy  # 等待MySQL健康检查通过后再启动后端
      redis:
        condition: service_started
    networks:
      - app-network
    volumes:
      - ./backend:/app            # 开发模式挂载源码实现热更新
      - /app/node_modules
    restart: on-failure
    # 健康检查:每10秒检测一次,超时5秒,连续3次失败则标记为unhealthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s           # 容器启动后15秒内不检测(给服务启动时间)

  # ======================== 数据库服务 ========================
  mysql:
    image: mysql:8.0              # 直接使用官方MySQL镜像
    ports:
      - "3306:3306"               # 数据库端口映射到宿主机
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}  # 根密码从.env读取
      - MYSQL_DATABASE=${DB_NAME}                   # 初始数据库名
      - MYSQL_USER=${DB_USER}                       # 应用数据库用户名
      - MYSQL_PASSWORD=${DB_PASSWORD}               # 应用数据库密码
    volumes:
      - db-data:/var/lib/mysql                    # 数据库数据持久化到命名卷
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql  # 初始化SQL脚本
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: on-failure

  # ======================== 缓存服务 ========================
  redis:
    image: redis:7-alpine         # 使用轻量版Redis镜像
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data          # Redis数据持久化
    networks:
      - app-network
    restart: on-failure
    # Redis健康检查:ping测试
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

# ======================== 网络定义 ========================
networks:
  app-network:
    driver: bridge                # 使用桥接网络驱动,容器间通过服务名通信

# =================------- 数据卷定义 ========================
volumes:
  db-data:                        # MySQL数据持久化卷
  redis-data:                     # Redis数据持久化卷

对应的.env文件:

# .env - 环境变量配置文件
# 所有敏感信息不要提交到代码仓库,加入.gitignore

# 数据库配置
MYSQL_ROOT_PASSWORD=MyRootPass123!
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=AppPass456!

# 前端配置
VITE_API_BASE_URL=http://localhost:3001

这个配置文件的每个部分都有对应的注释,新手朋友照着抄一份改改就能用。接下来讲怎么把它跑起来。

2.3 启动与管理命令

Docker Compose提供了一组非常直觉的命令来管理多容器应用:

# 构建并启动所有服务(-d表示后台运行)
docker compose -f docker-compose.yml up -d --build

# 查看运行状态
docker compose ps

# 查看某个服务的实时日志(-f表示follow)
docker compose logs -f backend

# 查看所有服务日志
docker compose logs -f

# 停止所有服务(不停止容器,下次up可以恢复)
docker compose stop

# 停止并删除容器(下次启动会重新创建)
docker compose down

# 停止、删除容器、网络,并清除未标记的匿名卷
docker compose down --remove-orphans

# 停止并删除所有数据卷(危险操作!会丢失数据库数据)
docker compose down -v

# 进入正在运行的容器执行命令(比如进入MySQL调试)
docker compose exec mysql mysql -u appuser -p appdb

# 重启单个服务
docker compose restart backend

# 重新构建某个服务的镜像(代码修改后)
docker compose build --no-cache backend

# 只更新某个服务(修改配置后)
docker compose up -d backend

三、服务间通信与网络配置

3.1 Compose内部服务发现机制

这是Docker Compose最强大的特性之一:同一Compose项目下的服务可以通过服务名直接互相访问,不需要记IP地址。

在上面的配置中,backend服务可以直接用mysql这个主机名访问MySQL,用redis访问Redis,用frontend访问前端。Compose内置了一个DNS解析器,会自动把服务名解析为对应的容器IP。

# 在backend的服务配置中,DB_HOST设置为mysql
# 在backend的代码中就可以这样写数据库连接:
# 连接字符串示例(Node.js + Sequelize)
# 不需要写localhost,因为容器间的localhost是各自自己
# 写成mysql就能正确连接到MySQL容器
# 同理Redis也直接用redis这个主机名

如果服务需要被宿主机上的其他程序访问,才需要配置ports映射。容器之间通信走内部网络,不走端口映射,这样更安全也更快。

3.2 多网络隔离

生产环境中,你通常希望把网络按功能分组隔离。比如数据库和缓存放在一个内部网络里,Web服务和消息队列放在另一个网络里,前端只跟Web服务网络通信。

# docker-compose.yml - 多网络隔离示例
# 技术栈:Docker Compose
services:
  frontend:
    image: nginx:latest
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network        # 前端需要访问后端网络才能和后端通信

  backend:
    build: ./backend
    networks:
      - backend-network
      - database-network       # 后端需要访问数据库网络

  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=testdb
    networks:
      - database-network       # 只有后端能访问,前端访问不到

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
  database-network:
    driver: bridge
    internal: true             # internal:true表示该网络不与外部通信

3.3 持久化数据存储

数据存储分两种:绑定挂载和命名卷。绑定挂载是把宿主机目录直接映射到容器里,适合开发环境(修改文件容器立即可见);命名卷由Docker管理,适合生产环境(性能更好,生命周期管理更清晰)。

上面的配置中已经用了两种方式的混合:开发模式下用绑定挂载实现热更新(./backend:/app),生产环境则用命名卷(db-data:/var/lib/mysql)。

管理数据卷的命令:

# 查看所有数据卷
docker volume ls

# 查看某个卷的详细信息
docker volume inspect db-data

# 查看卷的内容(需要先用一个临时容器挂载)
docker run --rm -v my-app_db-data:/data alpine ls /data

# 导出卷内容备份
docker run --rm -v my-app_db-data:/data -v $(pwd):/backup alpine cp -r /data/. /backup/mysql-backup/

# 删除指定卷
docker volume rm my-app_db-data

四、生产环境架构优化

开发环境的配置直接搬到生产环境是有问题的,需要从多个维度做优化。

4.1 资源限制

如果不限制容器资源,某个服务一旦内存泄漏或者CPU跑满,整个宿主机都可能被打垮。Compose支持在每个服务上配置资源限制:

# 生产环境资源限制配置示例
# 技术栈:Docker Compose
services:
  backend:
    build:
      context: ./backend
    deploy:
      resources:
        limits:
          cpus: "2.0"           # 最多使用2个CPU核心
          memory: 1G            # 最多使用1GB内存
        reservations:           # 预留资源(保证最低资源需求)
          cpus: "0.5"           # 至少保证0.5个CPU核心
          memory: 256M          # 至少保证256MB内存

注意:deploy.resources在docker compose standalone模式下部分字段可用,完整支持需要在Swarm模式下运行。如果你用的是Compose standalone,也可以用以下方式来限制:

# Compose standalone模式下的资源限制方式
services:
  backend:
    image: my-backend:latest
    mem_limit: 1g               # 内存限制1GB
    memswap_limit: 2g           # 内存+swap总限制
    cpus: 2                     # CPU限制2核
    cpu_shares: 512             # CPU权重(默认是1024)
    cpu_period: 100000          # CPU周期(微秒)
    cpu_quota: 200000           # CPU配额(微秒)

4.2 日志管理

生产环境如果不限日志,磁盘很快会被打满。配置日志驱动和轮转策略:

# 生产环境日志配置示例
version: "3.9"

x-logging-config: &default-logging  # 定义可复用的日志配置锚点
  driver: json-file                # 使用JSON文件格式
  options:
    max-size: "10m"               # 单个日志文件最大10MB
    max-file: "3"                 # 最多保留3个日志文件
    compress: "true"              # 压缩旧日志节省空间

services:
  backend:
    image: my-backend:latest
    logging: *default-logging      # 引用复用配置

  frontend:
    image: nginx:latest
    logging: *default-logging

  mysql:
    image: mysql:8.0
    logging:
      driver: json-file
      options:
        max-size: "50m"           # 数据库日志量大一些,允许50MB
        max-file: "5"
        compress: "true"

这个x-logging-config是一个扩展字段,Docker Compose不会把x-开头的字段解析成服务,但它支持YAML锚点引用,非常适合复用配置。

4.3 健康检查与自愈机制

健康检查让Compose能够自动判断服务是否真正可用。depends_on.condition: service_healthy配合健康检查,可以让服务按正确的顺序启动。

# 健康检查最佳实践示例
services:
  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p$MYSQL_ROOT_PASSWORD"]
      interval: 10s              # 每10秒检查一次
      timeout: 5s                # 检查超时5秒
      retries: 5                 # 连续5次失败标记为unhealthy
      start_period: 30s          # 启动后30秒内不检查(MySQL启动较慢)

  backend:
    image: my-backend:latest
    depends_on:
      mysql:
        condition: service_healthy   # 等MySQL健康检查通过后再启动
        restart: true               # MySQL重启时后端也跟着重启(Compose v2.17+)
    healthcheck:
      test: ["CMD", "node", "healthcheck.js"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s

  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"
    depends_on:
      mysql:
        condition: service_healthy

上面backend的restart: true是Compose v2.17新增的依赖重启功能,当被依赖的服务重启时,依赖方也会自动重启。这对于数据库重启导致连接池断开的场景非常实用。

五、进阶配置技巧

5.1 多文件与环境覆盖

不同环境(开发、测试、生产)的配置往往不同。Compose支持多文件叠加,用一个基础配置加多个环境覆盖文件:

# docker-compose.yml - 基础配置(所有环境共用)
# 技术栈:Docker Compose
version: "3.9"
services:
  backend:
    build: ./backend
    ports:
      - "3001:3001"
    environment:
      - DB_HOST=mysql
      - DB_PORT=3306
    networks:
      - default
  mysql:
    image: mysql:8.0
    volumes:
      - db-data:/var/lib/mysql
  redis:
    image: redis:7-alpine
  volumes:
    db-data:
# docker-compose.prod.yml - 生产环境覆盖配置
services:
  backend:
    ports:
      - "443:3001"               # 生产环境直接暴露443端口
    environment:
      - NODE_ENV=production
      - DB_PASSWORD=${DB_PASSWORD_PROD}  # 生产环境数据库密码
    deploy:
      resources:
        limits:
          cpus: "4.0"
          memory: 2G
    restart: always              # 生产环境设置always,异常自动重启

  mysql:
    image: mysql:8.0
    ports:
      - "3307:3306"              # 生产环境数据库端口改为3307避免冲突
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 15s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}  # 生产环境加密码
    volumes:
      - redis-data:/data
  volumes:
    redis-data:

启动时指定多文件:

# 先加载基础配置,再覆盖生产配置
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 注意:-f的顺序很重要,后面的文件覆盖前面的配置
# 如果要指定特定的.env文件
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
  --env-file .env.prod up -d

5.2 滚动更新与部署策略

Compose standalone模式下没有真正的滚动更新,但可以通过restart_policy和脚本模拟。在Swarm模式下则有完整的部署策略:

# Swarm模式下的部署策略(需要在Swarm集群中运行)
services:
  backend:
    image: my-backend:latest
    deploy:
      replicas: 3                 # 运行3个副本
      restart_policy:
        condition: on-failure     # 失败后自动重启
        delay: 10s                # 重启间隔10秒
        max_attempts: 5           # 最多重试5次
        window: 120s              # 计算窗口的时间范围
      update_config:              # 更新策略
        parallelism: 1            # 同时更新1个副本
        delay: 30s                # 每个副本更新完等30秒
        failure_action: rollback  # 失败自动回滚
        monitor: 30s              # 监控新副本30秒
      rollback_config:            # 回滚配置
        parallelism: 1
        delay: 10s
        failure_action: pause     # 回滚失败就暂停
      placement:                  # 放置约束
        constraints:
          - node.role == worker   # 只部署在worker节点
    ports:
      - "3001:3001"
    networks:
      - app-network

5.3 安全配置

生产环境中,安全是第一要务。以下几个方面值得特别注意:

# 安全配置最佳实践
services:
  backend:
    image: my-backend:latest
    user: "1001:1001"            # 以非root用户运行容器
    read_only: true               # 容器文件系统设为只读(防止被写入恶意文件)
    tmpfs:                        # 临时文件系统(允许写入特定目录)
      - /tmp:rw,noexec,nosuid,size=64m  # 允许写入/tmp,限制大小64MB
    security_opt:
      - no-new-privileges:true    # 禁止容器内进程提升权限
    cap_drop:                     # 移除所有Linux capabilities
      - ALL
    cap_add:                      # 只添加必需的capabilities
      - NET_BIND_SERVICE          # 如果需要绑定低端口
    pids_limit: 100               # 限制最大进程数为100,防止fork炸弹

  mysql:
    image: mysql:8.0
    user: "999:999"               # MySQL容器以非root用户运行
    read_only: false              # MySQL需要写入数据目录,所以不能只读
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - SETUID
      - SETGID
      - SYS_RESOURCE              # MySQL需要这个cap来调整文件权限
    pids_limit: 200

六、技术优缺点分析

6.1 优势

Docker Compose最大的优势是简化了多容器应用的编排。一个YAML文件就能描述整个应用的拓扑结构,团队里的新人拿到代码,一条docker compose up就能把整个环境跑起来,省去了繁琐的手动配置步骤。

配置即代码的理念让不同环境的配置差异变得透明可控。通过多文件叠加和env文件,开发、测试、生产环境共享同一份基础配置,各环境只需维护覆盖文件,大大降低了一致性风险。

Compose的声明式工作流让回滚变得简单。保存好旧版本的docker-compose.yml和.env文件,docker compose down && docker compose up -d就能回到之前的状态。

对于开发环境来说,Compose的热更新能力也非常实用。通过绑定挂载源码目录,前端改动刷新浏览器就能看到效果,后端改动重启容器就能生效,极大缩短了开发反馈周期。

6.2 不足

Compose单机编排,不支持多节点集群调度。如果你的应用需要跨多台机器部署,就需要用Kubernetes或者Swarm。

Compose的健康检查依赖容器内的检查命令,如果容器里没有curl或者mysqladmin之类的工具,健康检查会失败。解决办法是在Dockerfile里安装这些工具,或者使用脚本文件作为健康检查命令。

资源隔离和调度能力有限。虽然有资源限制配置,但Compose不具备Kubernetes那样的资源调度、自动扩缩容、服务发现发现、负载均衡等能力。生产环境中如果应用规模大了,需要考虑迁移到Kubernetes。

Compose本身不提供持久化配置管理,所有配置都写在YAML文件里。如果配置项太多或者频繁变更,维护起来会比较吃力,需要配合配置文件挂载或者配置中心来管理。

七、注意事项

配置文件里千万不要硬编码密码和密钥。使用.env文件配合${VARIABLE_NAME}语法引用环境变量,同时确保.env文件被加入了.gitignore。

# .gitignore中添加以下内容
.env
.env.*
!.env.example

提供一份不含敏感信息的.env.example文件给团队成员参考:

# .env.example - 环境变量示例文件(不含真实密码)
MYSQL_ROOT_PASSWORD=your-root-password-here
DB_NAME=your-db-name
DB_USER=your-db-user
DB_PASSWORD=your-db-password
REDIS_PASSWORD=your-redis-password

depends_on只控制启动顺序,不保证依赖服务已经真正就绪。比如MySQL容器启动后需要几秒才能真正接受连接,所以必须配合condition: service_healthy来确保MySQL健康检查通过后,依赖它的服务才开始启动。

数据卷是持久化数据的唯一可靠方式。如果只依赖容器的可写层存储数据,容器被删除后数据就没了。对于数据库、缓存等有状态服务,务必配置volumes。

生产环境使用命名卷而不是绑定挂载。绑定挂载(如./data:/app/data)有权限问题(宿主机文件系统权限与容器内不匹配)、性能问题(尤其跨文件系统)、安全风险(容器可以直接读写宿主机目录)。

容器内不要写绝对路径到宿主机。使用相对路径或者Compose管理的卷。因为容器在不同机器上运行时,宿主机路径可能不同。

在开发环境中使用restart: on-failure,生产环境中使用restart: always。这样开发时出错了方便排查,生产环境则保证服务持续可用。

八、文章总结

Docker Compose把多容器应用的编排从"手敲命令"提升到了"配置驱动",极大降低了多服务项目的开发和运维门槛。从开发环境的一条命令启动全部服务,到生产环境的资源限制、日志管理、健康检查、安全加固,Compose能覆盖绝大多数的中小型多容器应用场景。

掌握Compose的关键在于理解它的核心概念:services定义容器,networks定义通信,volumes定义持久化,depends_on定义启动顺序,healthcheck定义就绪判定。这五个概念组合在一起,就能描述出复杂的多服务应用拓扑。

对于单机部署、团队开发环境、中小型应用,Compose是性价比最高的选择。当应用规模增长到需要跨节点调度、自动扩缩容、复杂服务治理时,再考虑向Kubernetes演进。迁移的时候,Compose的配置逻辑和Kubernetes的资源模型有很多相似之处,理解Compose的基础知识不会白费。