docker-compose的实践
Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个单独的 YAML 文件 (docker-compose.yml
),你可以配置应用所需的所有服务(容器)、网络和卷,然后用一条命令,就能将它们作为一个整体,同时启动、停止和管理。
Docker Compose 解决了以下关键问题:
- 告别繁琐命令: 将所有容器的配置(镜像、端口、卷、环境变量、网络等)集中在一个 YAML 文件中,实现“配置即代码”,一目了然。
- 一键式环境管理: 使用
docker compose up
和docker compose down
这样简单的命令,就能创建和销毁整个应用环境,极大地提升了开发和测试的效率。 - 保证环境一致性: 任何团队成员,只要拿到同一个
docker-compose.yml
文件,就能在自己的机器上复现出完全一致的运行环境,彻底解决了“在我电脑上明明是好的”这个千古难题。
1. 组成
Docker Compose 的核心就是 docker-compose.yml
文件。它是一个声明式的配置文件,你告诉 Compose 你想要什么,它会负责实现。
关键组成部分:
services
: 文件的核心,定义了构成你应用的各个服务(容器)。每个服务都是一个独立的配置块。networks
: 定义服务之间通信的网络。如果不指定,Compose 会创建一个默认的桥接网络。volumes
: 定义数据持久化的存储卷。
1.1 示例
1 | # docker-compose.yml |
当你运行 docker compose up
,Compose 会:
- 读取这个文件。
- 看到
webapp
依赖cache
,所以先启动cache
服务。它会去拉取redis:alpine
镜像并启动一个名为projectname-cache-1
的容器。 - 接着,它会
build
当前目录的 Dockerfile,创建一个镜像。 - 然后启动
webapp
服务,创建一个名为projectname-webapp-1
的容器,并配置好端口映射、卷挂载和环境变量。 - 它还会创建一个默认网络,让
webapp
容器可以通过主机名cache
直接访问到cache
容器。
1.2 使用 .env
文件管理敏感信息
不要将数据库密码、API 密钥等硬编码在 docker-compose.yml
文件中。在同级目录下创建一个 .env
文件,Compose 会自动加载它。
1 | # .env file |
1 | # docker-compose.yml |
1.3 启动顺序
- 理解
depends_on
的局限性:depends_on
只保证容器的启动顺序,不保证容器内的应用已经就绪。你的 Web 应用可能在数据库服务完全初始化完成前就启动了,从而导致连接失败。 - 善用
healthcheck
: 为了解决上述问题,可以在docker-compose.yml
中为依赖的服务(如数据库)添加healthcheck
,然后在依赖方使用depends_on
的扩展语法来等待服务健康。
1.4 使用技巧
明确定义网络和卷: 虽然 Compose 会创建默认的网络和卷,但在更正式的场景下,最好在顶层的
networks
和volumes
块中明确定义它们,这样配置更清晰,也更容易管理。profiles
配置你可以为某些服务分配一个
profile
,比如debug
或testing
。只有当你使用--profile
标志启动时,这些服务才会被激活。这使得你的主docker-compose.yml
文件可以非常干净,同时又能包含各种场景下的服务。1
2
3
4
5
6
7
8services:
web:
# ...
db:
# ...
mailhog: # 一个邮件测试工具
image: mailhog/mailhog
profiles: ["dev"] # 只有在开发时才需要运行时使用
docker compose --profile dev up
才会启动mailhog
。
2. 使用技巧
2.1 和 k8s 对比
特性 | 一系列 docker run 命令 | docker-compose | Kubernetes (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 使用注意事项
在容器内使用
localhost
或127.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 | services: |
2.4 通过 extends
和 include
实现配置复用 (高级)
extends
是旧方式,而 include
是新版 Compose Specification 推荐的更强大的方式。
include
: 允许你将一个或多个 Compose 文件直接嵌入到当前配置中,可以来自本地路径,甚至是 Git 仓库的 URL。这比override
模式更灵活,因为它允许你构建模块化的服务块。
示例 (未来趋势): 想象你有一个公司级的通用 logging-stack.yml
。
1 | # in your project's docker-compose.yml |
这展示了你对 Compose 未来发展方向的理解和对构建可扩展、可维护配置的追求。
2.5 run
vs exec
:一次性任务与交互式会话
这是一个关键的区别,理解它能让你更专业地处理各种临时任务。
docker compose run <service> <command>
:- 作用: 启动一个新的、一次性的容器来执行命令,然后停止并移除该容器(除非你加了
--rm=false
)。它会使用服务的配置(如镜像、环境变量、网络),但会忽略ports
和restart
策略。 - 高手用法: 运行数据库迁移、安装依赖、执行测试脚本等一次性任务。
- 示例:
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 | # 启动所有服务,并指定 'worker' 服务启动 3 个副本 |
- 只能对无状态服务 (Stateless Services) 使用
scale
。
这是最最重要的一条规则!一个服务要能被水平扩展,它自身不能存储任何持久化状态(如用户会话、上传的文件等)。所有状态都必须外置到数据库、缓存(Redis)、对象存储(S3)等共享服务中。你绝对不能scale
一个数据库服务,否则会导致数据被写到不同容器里,造成数据分裂和不一致。 - 端口冲突 (Port Conflicts)。
如果你在docker-compose.yml
中为一个服务配置了固定的主机端口映射(如ports: ["8000:8000"]
),那么你无法对该服务进行scale
。因为第二个容器启动时会尝试绑定已经被第一个容器占用的主机端口8000
,从而导致失败。 - 这不是自动伸缩 (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 | # 启动 web 服务的一个临时容器,并【强制】它使用 yml 文件中定义的端口映射 |
为什么使用 docker-compose
为什么要使用 docker-compose,不能手动 docker build,然后再 docker run 吗?
为了达到和我们 docker-compose.yml
文件一样的效果,你需要执行以下命令:命令太长:非常容易输错,特别是 volume 映射部分。停止和删除容器需要 docker stop hexo_blog_dev
和 docker rm hexo_blog_dev
。想看日志需要 docker logs hexo_blog_dev
。命令都很分散。
构建镜像 (只需要在
Dockerfile
或代码变动后执行)1
docker build -t my-hexo-env .
运行容器 (每次启动环境都需要执行)
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)的理念。