Docker 教程

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 命令:

  1. 一个用于 PostgreSQL,可能还需要为数据创建一个命名卷。
  2. 一个用于 Redis。
  3. 一个用于 Flask 应用,需要将其链接到数据库和缓存容器、暴露端口,并挂载相关的代码。

这种方法很快就会变得极其复杂:

  • 手动编排:你必须按正确的顺序手动启动容器(例如,先启动数据库,再启动应用)。
  • 配置偏移:很容易在某个 docker run 命令中犯错,或者导致开发环境和生产环境之间的配置出现差异。
  • 可重复性差:要把这套环境分享给团队成员,意味着你需要分享一个冗长的脚本或一长串命令。
  • 清理繁琐:停止并删除所有相关的容器、网络和数据卷,需要执行大量的 docker stopdocker rmdocker network rmdocker 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_1child_key_2parent_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_nametechnologiessettingsdescription 都是顶级键。
  • technologies 的值是一个列表。
  • settings 的值是一个映射,它内部又包含了嵌套的映射(databasecache)。
  • 注意观察一致的缩进方式。hostportuser 都在同一个缩进级别,这说明它们都是 database 的子项。

3. docker-compose.yml 文件结构

docker-compose.yml 文件是所有 Docker Compose 项目的核心。它是一个单一的文件,定义了你应用栈中的所有服务、网络和数据卷。虽然我们将在下一章详细讲解服务、网络和数据卷的具体配置,但现在最重要的是理解构建这个文件的基础顶级键 (Top-Level Keys)

3.1 docker-compose.yml 的顶级键

一个典型的 docker-compose.yml 文件通常以 version 键开头,并至少包含一个 services 键。根据需要,它还可以包含 networksvolumes 来进行自定义配置。

  • version:
    • 作用:指定 Compose 文件格式的版本。这非常关键,因为不同的版本支持不同的功能和语法。
    • 现代实践:始终使用 3.x 系列的版本(例如 3.8, 3.9)。2.x 版本较老,功能也不够丰富。
    • 位置:必须是文件中的第一个键。
    • 示例
version: '3.8' # 使用 Compose 文件格式的 3.8 版本
  • services:
    • 作用:这是最重要的部分。你在这里定义构成你应用栈的每一个独立的容器(即服务)。该键下的每一个服务条目,都代表了一个关于如何构建、配置和运行 Docker 容器的定义。
    • 结构:在 services 下面,你可以定义任意的服务名称(例如 webappdatabasecache)。每个服务名称随之成为一个键,而它的值就是一个映射,包含了该特定容器的所有配置细节。
    • 位置:这是一个顶级键,直接位于 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_frontendapi_backenddatabase 都是用户自定义的服务名称。它们具体的所有设置(比如使用什么镜像、暴露什么端口等)都将嵌套在这些名称下方。

  • networks (可选):
    • 作用:为你的服务定义自定义网络。默认情况下,Compose 会自动创建一个默认网络,但自定义网络能提供更好的隔离性和控制力。
    • 结构:在 networks 下面,你可以定义任意的网络名称。每个网络名称成为一个键,它的值是一个映射,包含该网络的配置细节(例如 driver 驱动)。
    • 位置:顶级键,与 services 同级。
    • 示例(概念结构):
version: '3.8'
services:
  # ... 服务定义 ...
networks: # 这个代码块为我们的服务定义了自定义网络。
          # 之后我们可以显式地将服务连接到这些网络上。

  app_network: # 这是我们的自定义网络名称。
    # app_network 的配置细节将放在这里。
    # (例如:driver, external, ipam)
  • volumes (可选):
    • 作用:定义用于持久化数据存储的命名卷。虽然绑定挂载(Bind Mounts,将宿主机路径挂载到容器)是在每个具体的服务内配置的,但命名卷(Named Volumes)通常在这里进行全局定义,以便更轻松地管理和复用。
    • 结构:在 volumes 下面,你可以定义任意的数据卷名称。每个卷名称成为一个键,它的值是一个映射,包含该数据卷的配置细节(例如 driver 驱动)。
    • 位置:顶级键,与 servicesnetworks 同级。
    • 示例(概念结构):
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' 服务的一个属性。

解析:这个示例清晰地展示了 versionservices 这两个顶级键。在 services 下方,我们定义了一个名为 web 的服务。注释说明了 web 的所有具体配置(如 imageports)都是嵌套在其下方并进行缩进的。

4.2 示例 2:多服务应用骨架(Web 应用 + 数据库)

这个示例对结构进行了扩展以包含多个服务,同时也暗示了自定义的 networksvolumes,展示了它们的顶级放置位置。

# 这是一个多服务应用程序(例如,带数据库的 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=数据库持久化存储"

解析:这个示例在第一个示例的基础上构建,展示了两个服务:webappdatabase,演示了多个服务定义在 services 键下是如何组织的。它同时引入了 networksvolumes 顶级键,解释了它们的作用并展示了配置应当放置的位置。这展示了一个更复杂的 Compose 文件的完整层级结构。