docker-compose的实践

Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个单独的 YAML 文件 (docker-compose.yml),你可以配置应用所需的所有服务(容器)、网络和卷,然后用一条命令,就能将它们作为一个整体,同时启动、停止和管理。

Docker Compose 解决了以下关键问题:

  • 告别繁琐命令: 将所有容器的配置(镜像、端口、卷、环境变量、网络等)集中在一个 YAML 文件中,实现“配置即代码”,一目了然。
  • 一键式环境管理: 使用 docker compose updocker compose down 这样简单的命令,就能创建和销毁整个应用环境,极大地提升了开发和测试的效率。
  • 保证环境一致性: 任何团队成员,只要拿到同一个 docker-compose.yml 文件,就能在自己的机器上复现出完全一致的运行环境,彻底解决了“在我电脑上明明是好的”这个千古难题。

1. 组成

Docker Compose 的核心就是 docker-compose.yml 文件。它是一个声明式的配置文件,你告诉 Compose 你想要什么,它会负责实现。

关键组成部分:

  • services: 文件的核心,定义了构成你应用的各个服务(容器)。每个服务都是一个独立的配置块。
  • networks: 定义服务之间通信的网络。如果不指定,Compose 会创建一个默认的桥接网络。
  • volumes: 定义数据持久化的存储卷。

1.1 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# docker-compose.yml

# version 字段在 V2+ 规范中是可选的,现在通常省略
services:
# 定义第一个服务,我们叫它 'webapp'
webapp:
# 基于当前目录下的 Dockerfile 来构建镜像
build: .
# 将主机的 8080 端口映射到容器的 80 端口
ports:
- "8080:80"
# 将当前目录挂载到容器的 /app 目录,方便代码热更新
volumes:
- .:/app
# 设置环境变量
environment:
- REDIS_HOST=cache
# 声明此服务依赖于 'cache' 服务
depends_on:
- cache

# 定义第二个服务,我们叫它 'cache'
cache:
# 直接使用官方的 Redis 镜像
image: "redis:alpine"

当你运行 docker compose up,Compose 会:

  1. 读取这个文件。
  2. 看到 webapp 依赖 cache,所以先启动 cache 服务。它会去拉取 redis:alpine 镜像并启动一个名为 projectname-cache-1 的容器。
  3. 接着,它会 build 当前目录的 Dockerfile,创建一个镜像。
  4. 然后启动 webapp 服务,创建一个名为 projectname-webapp-1 的容器,并配置好端口映射、卷挂载和环境变量。
  5. 它还会创建一个默认网络,让 webapp 容器可以通过主机名 cache 直接访问到 cache 容器。

1.2 使用 .env 文件管理敏感信息

不要将数据库密码、API 密钥等硬编码在 docker-compose.yml 文件中。在同级目录下创建一个 .env 文件,Compose 会自动加载它。

1
2
# .env file
POSTGRES_PASSWORD=mysecretpassword
1
2
3
4
5
6
# docker-compose.yml
services:
db:
image: postgres
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # 从 .env 文件读取

1.3 启动顺序

  1. 理解 depends_on 的局限性: depends_on 只保证容器的启动顺序,不保证容器内的应用已经就绪。你的 Web 应用可能在数据库服务完全初始化完成前就启动了,从而导致连接失败。
  2. 善用 healthcheck: 为了解决上述问题,可以在 docker-compose.yml 中为依赖的服务(如数据库)添加 healthcheck,然后在依赖方使用 depends_on 的扩展语法来等待服务健康。

1.4 使用技巧

  1. 明确定义网络和卷: 虽然 Compose 会创建默认的网络和卷,但在更正式的场景下,最好在顶层的 networksvolumes 块中明确定义它们,这样配置更清晰,也更容易管理。

  2. profiles 配置

    你可以为某些服务分配一个 profile,比如 debugtesting。只有当你使用 --profile 标志启动时,这些服务才会被激活。这使得你的主 docker-compose.yml 文件可以非常干净,同时又能包含各种场景下的服务。

    1
    2
    3
    4
    5
    6
    7
    8
    services:
    web:
    # ...
    db:
    # ...
    mailhog: # 一个邮件测试工具
    image: mailhog/mailhog
    profiles: ["dev"] # 只有在开发时才需要

    运行时使用 docker compose --profile dev up 才会启动 mailhog

2. 使用技巧

2.1 和 k8s 对比

特性一系列 docker run 命令docker-composeKubernetes (k8s)
核心目的运行单个或少数几个容器编排和管理单机上的多容器应用编排和管理集群中的容器化应用
主要场景快速测试、简单脚本本地开发、自动化测试、小型单机生产部署大规模生产环境、高可用、自动扩缩容
管理方式命令式、手动、繁琐声明式 (YAML)、项目级管理、简单高效声明式 (YAML)、集群级管理、功能强大但复杂
网络需要手动创建和连接网络 (docker network create ...)自动创建项目专属网络,服务名即主机名拥有复杂的网络模型(Pod、Service、Ingress)
伸缩性手动启停容器docker compose up --scale service_name=N (单机伸缩)自动水平伸缩 (HPA)、跨多台物理机
学习曲线低(你已掌握)中等(掌握 YAML 和核心概念即可)高(概念多,体系庞大)

一句话总结:docker run 是手动挡,docker-compose 是自动挡小汽车(适合日常通勤和短途旅行),Kubernetes 是大型客机(适合跨国、大规模运输)。

2.2 使用注意事项

  • 在容器内使用 localhost127.0.0.1 进行服务间通信。

    • 原因: 每个容器都有自己独立的网络命名空间,localhost 指向的是容器自身,而不是宿主机或其他容器。

    • 如何避免: 牢记!在 Compose 创建的网络中,可以直接使用服务名作为主机名进行通信。例如,上例中的 webapp 服务要连接 Redis,连接地址应该是 redis://cache:6379,而不是 redis://localhost:6379

  • 卷挂载路径混淆。

    • 原因: volumes 的格式是 HOST_PATH:CONTAINER_PATH,新手很容易搞反,或者在 Windows 上因为路径格式问题(如 C:\Users\... vs /c/Users/...)而出错。
    • 如何避免: 明确记住“左主右容”(左边是宿主机,右边是容器)。尽量使用相对路径 ./ 来指定宿主机路径,以增加可移植性。对于复杂数据,优先使用命名卷,让 Docker 自己管理,更不容易出错。

2.3 Networking: expose vs ports

  • ports: 我们很熟悉了,格式是 HOST:CONTAINER,它将容器端口发布到宿主机网络上,让外部可以访问。
  • expose: 格式是 ["PORT"],它只在 Compose 创建的内部网络中暴露端口。宿主机和其他外部网络无法访问这个端口,但网络内的其他服务(如 webapp)可以访问。

高手用法: 对于数据库、缓存这类不应该被外界直接访问的服务,只使用 expose 而不使用 ports。这是一种简单有效的安全加固。

1
2
3
4
5
6
services:
database:
image: postgres
expose:
- "5432" # 只在内部网络暴露5432端口
# 没有 'ports' 字段,所以你无法从宿主机直接连接它

2.4 通过 extendsinclude 实现配置复用 (高级)

extends 是旧方式,而 include 是新版 Compose Specification 推荐的更强大的方式。

  • include: 允许你将一个或多个 Compose 文件直接嵌入到当前配置中,可以来自本地路径,甚至是 Git 仓库的 URL。这比 override 模式更灵活,因为它允许你构建模块化的服务块。

示例 (未来趋势): 想象你有一个公司级的通用 logging-stack.yml

1
2
3
4
5
6
7
8
9
10
11
# in your project's docker-compose.yml
include:
- path: ./common-services/postgres.yml
- path: ./common-services/redis.yml
- project: my-monitoring-stack
path: git@github.com:my-org/infra-compose.git/logging-stack/compose.yml

services:
# a service specific to your project
my-app:
# ...

这展示了你对 Compose 未来发展方向的理解和对构建可扩展、可维护配置的追求。

2.5 run vs exec:一次性任务与交互式会话

这是一个关键的区别,理解它能让你更专业地处理各种临时任务。

  • docker compose run <service> <command>:
    • 作用: 启动一个新的、一次性的容器来执行命令,然后停止并移除该容器(除非你加了 --rm=false)。它会使用服务的配置(如镜像、环境变量、网络),但会忽略 portsrestart策略。
    • 高手用法: 运行数据库迁移、安装依赖、执行测试脚本等一次性任务。
    • 示例: docker compose run --rm webapp npm install (在新容器里安装依赖后自动清理)。
  • docker compose exec <service> <command>:
    • 作用: 在一个已经运行的容器里执行命令。
    • 高手用法: 进入正在运行的容器进行调试、查看日志文件、执行实时命令。
    • 示例: docker compose exec database bash (获取一个正在运行的数据库容器的 shell)。

2.6 scale 的用法

scale 是 Docker Compose 中一个非常强大且关键的功能,掌握它意味着你真正开始将 Compose 从“运行几个容器的工具”看作是“编排应用的平台”。

这是一个高手必须理解的概念,因为它直击水平扩展的核心。

当你用 scale 将一个名为 api 的服务扩展到 3 个副本时,Compose 会创建三个容器:project-api-1, project-api-2, project-api-3

现在,如果网络中的另一个服务(比如 frontend)尝试通过服务名 api 来访问它,会发生什么?

Docker Compose 内置的 DNS 服务会自动进行轮询(Round-Robin)负载均衡。

1
2
3
4
5
6
7
8
# 启动所有服务,并指定 'worker' 服务启动 3 个副本
docker compose up -d --scale worker=3

# 在不重启其他服务的情况下,将 'worker' 服务的副本数调整到 5
docker compose up -d --scale worker=5

# 缩减 'worker' 服务的副本数到 1
docker compose up -d --scale worker=1
  1. 只能对无状态服务 (Stateless Services) 使用 scale
    这是最最重要的一条规则!一个服务要能被水平扩展,它自身不能存储任何持久化状态(如用户会话、上传的文件等)。所有状态都必须外置到数据库、缓存(Redis)、对象存储(S3)等共享服务中。你绝对不能 scale 一个数据库服务,否则会导致数据被写到不同容器里,造成数据分裂和不一致。
  2. 端口冲突 (Port Conflicts)。
    如果你在 docker-compose.yml 中为一个服务配置了固定的主机端口映射(如 ports: ["8000:8000"]),那么你无法对该服务进行 scale。因为第二个容器启动时会尝试绑定已经被第一个容器占用的主机端口 8000,从而导致失败。
  3. 这不是自动伸缩 (Auto-Scaling)。
    scale 是一个手动操作。你必须自己决定何时增加或减少副本数。它不会根据CPU负载自动调整。真正的自动伸缩是 Kubernetes HPA (Horizontal Pod Autoscaler) 这类更高级编排工具的功能。

3. 提问问题

docker-compose run 和 up 区别

我们用一个生动的比喻来开始:

  • docker-compose up: 就像是举办一个派对。你发出邀请(启动所有服务),打开大门(映射端口),放上音乐,确保酒水食物都已就位(服务之间网络联通)。这个派对会一直进行下去,直到你喊停(docker-compose down)。
  • docker-compose run: 就像是请一个水管工来修水管。你专门请他来做一件特定的事(执行一个命令)。他会带着工具进来,完成工作,然后就离开。他不会去开你家的音响(不会映射端口),也不会去跟其他客人聊天(默认不启动其他服务)。

你可以使用 docker-compose run 的一个特殊标志:--service-ports

1
2
# 启动 web 服务的一个临时容器,并【强制】它使用 yml 文件中定义的端口映射
docker-compose run --service-ports web

为什么使用 docker-compose

为什么要使用 docker-compose,不能手动 docker build,然后再 docker run 吗?

为了达到和我们 docker-compose.yml 文件一样的效果,你需要执行以下命令:命令太长:非常容易输错,特别是 volume 映射部分。停止和删除容器需要 docker stop hexo_blog_devdocker rm hexo_blog_dev。想看日志需要 docker logs hexo_blog_dev。命令都很分散。

  1. 构建镜像 (只需要在 Dockerfile 或代码变动后执行)

    1
    docker build -t my-hexo-env .
  2. 运行容器 (每次启动环境都需要执行)

    1
    docker run -d --name hexo_blog_dev -p 4000:4000 -v "$(pwd):/app" -v /app/node_modules my-hexo-env

使用 docker-compose

你把上面那一长串 docker run 命令的所有配置参数,用一种清晰、结构化的方式写进了 docker-compose.yml 文件里。这个 YAML 文件就像一个“容器启动说明书”。这种方式的优点:

  • 声明式,可读性高:你一看 docker-compose.yml 文件,就知道这个服务需要哪个镜像、映射了什么端口、挂载了哪些目录。它本身就是一份文档。
  • 命令极简:
    • 启动并构建:docker-compose up --build -d
    • 日常启动:docker-compose up -d
    • 停止并移除:docker-compose down
    • 查看日志:docker-compose logs
      这些命令简短、直观,并且 Compose 会自动帮你处理容器命名、网络等问题。
  • 可复现性强:任何人拿到你的项目,只要装了 Docker,运行 docker-compose up 就能得到一个一模一样的环境。这就是“基础设施即代码”(Infrastructure as Code)的理念。

4. 参考资料