Docker Compose 基础:理解 YAML 文件结构与核心概念
当构建简单的单容器应用时,docker run 命令就能提供启动和配置容器所需的全部功能。然而,现实世界中的应用程序很少有这么简单的。大多数现代应用由多个相互连接的服务组成:Web 服务器、应用后端、数据库、缓存以及其他可能的辅助服务。如果使用独立的 docker run 命令来单独管理这些服务,不仅过程繁琐、容易出错,而且很难在不同环境中保持一致的复现。
这时,Docker Compose 就派上用场了。Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。它允许你在一个单一的文件中配置整个应用栈,编排所有服务、网络连接以及数据卷。本章将为你介绍 Docker Compose 的核心概念以及其配置文件的基础结构。该配置文件使用 YAML(YAML Ain't Markup Language)语言编写。理解这种 YAML 文件结构,是高效管理复杂多容器 Docker 应用的基石。
1. 理解 Docker Compose 及其作用
在深入研究语法之前,必须要明白为什么 Docker Compose 如此重要。正如你在前面的模块中学到的,Docker 允许你将单个组件容器化。你可以构建镜像、运行容器,并管理网络和数据卷。但是,假设你有一个包含以下组件的应用:
- 一个 Python Flask Web 应用(容器 1)
- 一个 PostgreSQL 数据库(容器 2)
- 一个 Redis 缓存(容器 3)
如果在没有 Compose 的情况下运行这个应用,你需要执行多个 docker run 命令:
- 一个用于 PostgreSQL,可能还需要为数据创建一个命名卷。
- 一个用于 Redis。
- 一个用于 Flask 应用,需要将其链接到数据库和缓存容器、暴露端口,并挂载相关的代码。
这种方法很快就会变得极其复杂:
- 手动编排:你必须按正确的顺序手动启动容器(例如,先启动数据库,再启动应用)。
- 配置偏移:很容易在某个
docker run命令中犯错,或者导致开发环境和生产环境之间的配置出现差异。 - 可重复性差:要把这套环境分享给团队成员,意味着你需要分享一个冗长的脚本或一长串命令。
- 清理繁琐:停止并删除所有相关的容器、网络和数据卷,需要执行大量的
docker stop、docker rm、docker network rm、docker volume rm命令。
Docker Compose 通过允许你在单一的 docker-compose.yml 文件中定义整个应用栈,完美解决了这些问题。这个文件就像一张蓝图,具体指定了:
- Services (服务):哪些容器组成了你的应用(如 Web 应用、数据库、缓存)。
- Images (镜像):每个服务使用哪个 Docker 镜像。
- Dependencies (依赖关系):服务之间的先后关系(例如,Web 应用依赖于数据库)。
- Networking (网络):服务之间如何进行通信。
- Volumes (数据卷):如何管理每个服务的持久化数据。
有了这一个 docker-compose.yml 文件,你只需一条命令(docker compose up)就能启动整个应用,用另一条命令(docker compose down)将其停止,并且可以将完全一致的环境配置分享给任何人。
2. Docker Compose 的 YAML 语法入门
Docker Compose 的配置文件是用 YAML 编写的。YAML(YAML Ain't Markup Language,即“YAML 不是一种标记语言”)是一种对人类极其友好的数据序列化标准,因其出色的可读性,常被用于编写配置文件。如果你接触过 JSON 或 XML,你会发现 YAML 在概念上与它们很相似,但它的语法严重依赖缩进,而不是大括号或标签。
理解基础的 YAML 语法对于编写有效的 Docker Compose 文件至关重要。
2.1 核心 YAML 概念
- 键值对 (Key-Value Pairs):这是最基本的构建块。一个键(Key)后面跟着一个冒号(
:),然后是一个空格,最后是它的值(Value)。
key: value示例:
name: My Application
version: 1.0缩进 (Indentation):YAML 使用空格缩进来表示结构和层级关系。必须使用空格进行缩进,绝对不能使用 Tab 键。 空格的数量可以变化,但在同一个逻辑块内必须保持一致。通常,每个缩进级别使用 2 个或 4 个空格。缩进错误是 YAML 文件中最常见的报错原因。
parent_key:
child_key_1: value_1
child_key_2:
grandchild_key: value_2在这个例子中,child_key_1 和 child_key_2 是 parent_key 的子项,因为它们向内缩进了一层。grandchild_key 则是 child_key_2 的子项。
- 列表/序列 (Lists / Sequences):列表由一个短横线(-)加上一个空格开头来表示每一项。
list_name:
- item_1
- item_2
- item_3列表内部可以包含键值对或者其他列表。示例:
fruits:
- apple
- banana
- orange或者是一个包含映射(字典)的列表:
people:
- name: Alice
age: 30
- name: Bob
age: 25字符串 (Strings):在 YAML 中,字符串通常不需要加引号。但是,如果字符串包含特殊字符(如 :, #, &, *, !, |, >, {, [, ], ,, ?),或者以特殊字符开头,亦或者可能被误解析为数字或布尔值(如 yes, no, true, false),则强烈建议使用单引号 (') 或双引号 (") 将其包裹起来。示例:
unquoted_string: Hello world
quoted_string_1: "这是一个内部带有冒号的字符串: 这里"
quoted_string_2: '12345' # '12345' 会被解析为字符串,而 12345 会被解析为整数对于多行字符串,你可以使用块标量符(block scalars):
|(字面量块标量):保留所有的换行符。>(折叠块标量):将换行符折叠转换为空格(除非段落之间有空行隔开)。示例:
literal_text: |
这是一个多行
字符串。
换行符会被原样保留。
folded_text: >
这也是一个多行
字符串。但是换行符
会被折叠转换为空格。- 注释 (Comments):注释以井号 (#) 开头,一直延伸到该行的末尾。
# 这是一整行的注释
key: value # 这是一个行尾注释2.2 实战 YAML 示例:基础结构
让我们看一个简单的 YAML 文件,来巩固这些概念。
# 这是一个演示基础语法的示例 YAML 文件。
# 在根层级的一个简单的键值对
application_name: MyAwesomeApp
# 包含一个简单字符串列表作为值的键
technologies:
- Docker
- Python
- PostgreSQL
- Redis
# 包含一个映射(字典)作为值的键
settings:
database:
host: localhost
port: 5432
user: admin
cache:
enabled: true
max_memory: '256mb' # 使用字符串值,因为 '256mb' 不是纯数字
# 一个多行字符串的示例
description: |
该应用程序利用了现代
技术栈以实现高性能和高扩展性。
它包含一个 Web API、一个健壮的数据库,
以及一个快速的缓存层。在这个例子中:
application_name、technologies、settings和description都是顶级键。technologies的值是一个列表。settings的值是一个映射,它内部又包含了嵌套的映射(database、cache)。- 注意观察一致的缩进方式。
host、port、user都在同一个缩进级别,这说明它们都是 database 的子项。
3. docker-compose.yml 文件结构
docker-compose.yml 文件是所有 Docker Compose 项目的核心。它是一个单一的文件,定义了你应用栈中的所有服务、网络和数据卷。虽然我们将在下一章详细讲解服务、网络和数据卷的具体配置,但现在最重要的是理解构建这个文件的基础顶级键 (Top-Level Keys)。
3.1 docker-compose.yml 的顶级键
一个典型的 docker-compose.yml 文件通常以 version 键开头,并至少包含一个 services 键。根据需要,它还可以包含 networks 和 volumes 来进行自定义配置。
- version:
- 作用:指定 Compose 文件格式的版本。这非常关键,因为不同的版本支持不同的功能和语法。
- 现代实践:始终使用 3.x 系列的版本(例如 3.8, 3.9)。2.x 版本较老,功能也不够丰富。
- 位置:必须是文件中的第一个键。
- 示例:
version: '3.8' # 使用 Compose 文件格式的 3.8 版本- services:
- 作用:这是最重要的部分。你在这里定义构成你应用栈的每一个独立的容器(即服务)。该键下的每一个服务条目,都代表了一个关于如何构建、配置和运行 Docker 容器的定义。
- 结构:在
services下面,你可以定义任意的服务名称(例如webapp、database、cache)。每个服务名称随之成为一个键,而它的值就是一个映射,包含了该特定容器的所有配置细节。 - 位置:这是一个顶级键,直接位于 version 之下(或同级)。
- 示例(概念结构 - 具体配置将在下一章讲解):
version: '3.8'
services: # 这个代码块定义了我们应用的所有容器。
# 'services' 下的每一个键都是一个容器/服务的名称。
web_frontend: # 这是我们 Web 用户界面的定义。
# web_frontend 服务的配置细节将放在这里,
# 向内缩进两个空格(或你采用的统一缩进量)。
# 示例(这里不展开): image, build, ports, volumes, environment, depends_on.
api_backend: # 这是我们后端 API 服务器的定义。
# api_backend 服务的配置细节将放在这里。
database: # 这是我们数据库服务器的定义。
# database 服务的配置细节将放在这里。在这个例子中,web_frontend、api_backend 和 database 都是用户自定义的服务名称。它们具体的所有设置(比如使用什么镜像、暴露什么端口等)都将嵌套在这些名称下方。
- networks (可选):
- 作用:为你的服务定义自定义网络。默认情况下,Compose 会自动创建一个默认网络,但自定义网络能提供更好的隔离性和控制力。
- 结构:在
networks下面,你可以定义任意的网络名称。每个网络名称成为一个键,它的值是一个映射,包含该网络的配置细节(例如driver驱动)。 - 位置:顶级键,与
services同级。 - 示例(概念结构):
version: '3.8'
services:
# ... 服务定义 ...
networks: # 这个代码块为我们的服务定义了自定义网络。
# 之后我们可以显式地将服务连接到这些网络上。
app_network: # 这是我们的自定义网络名称。
# app_network 的配置细节将放在这里。
# (例如:driver, external, ipam)- volumes (可选):
- 作用:定义用于持久化数据存储的命名卷。虽然绑定挂载(Bind Mounts,将宿主机路径挂载到容器)是在每个具体的服务内配置的,但命名卷(Named Volumes)通常在这里进行全局定义,以便更轻松地管理和复用。
- 结构:在
volumes下面,你可以定义任意的数据卷名称。每个卷名称成为一个键,它的值是一个映射,包含该数据卷的配置细节(例如driver驱动)。 - 位置:顶级键,与
services和networks同级。 - 示例(概念结构):
version: '3.8'
services:
# ... 服务定义 ...
volumes: # 这个代码块定义了可以在服务之间共享的命名卷
# 或者用于持久化数据存储的卷。
db_data: # 这是我们用于存放数据库数据的持久化数据卷名称。
# db_data 的配置细节将放在这里。
# (例如:driver, external)
app_logs: # 这是另一个命名卷,或许用于存放应用程序日志。
# app_logs 的配置细节将放在这里。3.2 docker-compose.yml 顶级结构总结一览
version: '3.8' # 永远以 version 键开头。
services: # 在这里将独立的容器定义为服务。
# 每个服务代表一个容器(或一组相同的容器)。
service_name_1:
# --- service_name_1 的配置写在这里 ---
# 比如:image, ports, volumes, environment, networks, depends_on
service_name_2:
# --- service_name_2 的配置写在这里 ---
# 比如:image, build, command, restart, logging
# ... 更多服务 ...
networks: # (可选) 定义用于服务间通信的自定义网络。
network_name_1:
# --- network_name_1 的配置写在这里 ---
# 比如:driver, driver_opts, external
network_name_2:
# --- network_name_2 的配置写在这里 ---
# 比如:ipam, attachable
# ... 更多网络 ...
volumes: # (可选) 定义用于持久化数据存储的命名卷。
volume_name_1:
# --- volume_name_1 的配置写在这里 ---
# 比如:driver, driver_opts, external, labels
volume_name_2:
# --- volume_name_2 的配置写在这里 ---
# 比如:name
# ... 更多数据卷 ...4. 结构示例演示
让我们来看看这些结构元素是如何组合成实际的 docker-compose.yml 文件的。请注意,这里的重点纯粹是结构,暂时不会深入讲解每个服务、网络或数据卷内部的具体配置选项。
4.1 示例 1:基础的 Web 服务器骨架
这个示例为一个单一的 Web 服务器服务搭建了骨架。请注意,image 指令被注释掉了,这强调了我们稍后会解释它,但它的放置位置是 YAML 结构的一部分。
# 这是一个简单 Web 服务器应用程序的 docker-compose.yml 文件
# 本文件演示了基础结构:version 以及单个服务定义。
version: '3.8' # 指定 Docker Compose 文件格式的版本。
# 它应该永远在第一行。
services: # 'services' 键是我们定义所有独立容器(服务)的地方。
# 'services' 下的每一个键都代表一个单一的服务。
web: # 这是我们给 Web 服务器服务起的名称。
# 在这个键的下方,我们将定义关于 'web' 容器的所有具体配置。
# 例如,使用哪个 Docker 镜像,暴露哪些端口等。
# image: nginx:latest # 这一行(以及类似的行)将定义 'web' 的实际镜像。
# 它的完整解释将在下一章中介绍。
# 目前,我们只关注这个配置在 YAML 结构中 *应该放在哪里*。
# ports:
# - "80:80" # 这里是定义端口映射的示例。
# 缩进在这里非常关键:'- "80:80"' 是一个名为 'ports' 的列表中的一项,
# 而 'ports' 本身是 'web' 服务的一个属性。解析:这个示例清晰地展示了 version 和 services 这两个顶级键。在 services 下方,我们定义了一个名为 web 的服务。注释说明了 web 的所有具体配置(如 image 或 ports)都是嵌套在其下方并进行缩进的。
4.2 示例 2:多服务应用骨架(Web 应用 + 数据库)
这个示例对结构进行了扩展以包含多个服务,同时也暗示了自定义的 networks 和 volumes,展示了它们的顶级放置位置。
# 这是一个多服务应用程序(例如,带数据库的 Web 应用)的 docker-compose.yml 文件
# 本文件演示了包含多个服务的顶级结构,
# 并包含了自定义网络和数据卷的占位部分。
version: '3.8' # 使用现代的 Compose 文件格式版本。
services: # 这部分定义了我们应用中所有不同的组成部分(容器)。
webapp: # 这个服务代表我们的主 Web 应用容器。
# 它所有的具体配置都将嵌套在这里。
# 例如,我们可能会指定从 Dockerfile 构建,
# 设定环境变量,以及它监听哪个端口。
# build: . # 从当前目录的 Dockerfile 构建镜像的示例。
# environment: # 设置环境变量的示例。
# DATABASE_URL: postgres://user:password@database:5432/mydb
# depends_on: # 声明服务依赖关系的示例。
# - database
database: # 这个服务代表我们的数据库容器(例如 PostgreSQL, MySQL)。
# 它的具体配置将嵌套在这里。
# 这将包括数据库镜像、凭据的环境变量,
# 以及映射一个用于持久化数据的数据卷。
# image: postgres:13 # 指定 Docker 镜像的示例。
# environment:
# POSTGRES_DB: mydb
# POSTGRES_USER: user
# POSTGRES_PASSWORD: password
# volumes:
# - db_data:/var/lib/postgresql/data # 挂载一个命名卷的示例。
# --- 可选的顶级层级部分 ---
# 这些部分并非总是必须的,但常被用于
# 高级网络配置或持久化数据管理。
# networks: # 这部分定义了自定义网络。
# # 之后可以显式地将服务连接到这些网络,
# # 提供比默认网络更好的隔离性和组织结构。
#
# app_internal_network: # 我们的自定义网络名称。
# # driver: bridge # 指定网络驱动的示例。
# # ipam: # IP 地址管理 (IPAM) 配置示例。
# # config:
# # - subnet: 172.20.0.0/16
# volumes: # 这部分定义了用于持久化存储的命名卷。
# # 命名卷由 Docker 统一管理,并且如模块 4 所述,
# # 它是持久化存储数据的推荐方式。
#
# db_data: # 我们用于存放数据库数据的卷名称。
# # driver: local # 指定卷驱动的示例。
# # labels:
# # - "com.example.description=数据库持久化存储"解析:这个示例在第一个示例的基础上构建,展示了两个服务:webapp 和 database,演示了多个服务定义在 services 键下是如何组织的。它同时引入了 networks 和 volumes 顶级键,解释了它们的作用并展示了配置应当放置的位置。这展示了一个更复杂的 Compose 文件的完整层级结构。