Docker Compose 基础指南:轻松驾驭多容器应用
在现代软件开发和部署中,容器化技术,尤其是 Docker,已经成为不可或缺的一部分。Docker 允许我们将应用程序及其依赖项打包到一个轻量级、可移植的容器中,确保在不同环境中拥有一致的运行表现。然而,当应用程序变得复杂,需要由多个相互依赖的服务(例如 Web 服务器、数据库、缓存服务、消息队列等)组成时,单独管理每个 Docker 容器的启动、连接和生命周期会变得非常繁琐和容易出错。
这时,Docker Compose 应运而生。它是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个简单的 YAML 文件(通常是 docker-compose.yml
),你可以配置应用程序所需的所有服务、网络和卷,然后使用一条命令即可启动、停止和管理整个应用集群。
本指南将深入探讨 Docker Compose 的基础知识,涵盖其核心概念、常用命令、docker-compose.yml
文件详解以及一个实战示例,帮助你从入门到熟练掌握这个强大的工具。
1. 为什么需要 Docker Compose?
在了解具体用法之前,我们先明确使用 Docker Compose 的核心优势:
- 简化多容器管理:想象一下,你需要手动运行多个
docker run
命令,并配置它们之间的网络连接、端口映射、卷挂载以及启动依赖关系。这不仅命令冗长,而且极易出错。Docker Compose 将所有这些配置集中在一个docker-compose.yml
文件中,使用docker-compose up
即可一键启动所有服务。 - 环境一致性:
docker-compose.yml
文件定义了应用程序的完整环境,包括服务版本、配置、网络和数据卷。这确保了开发、测试和生产环境之间的高度一致性,减少了“在我机器上能跑”的问题。 - 开发效率提升:开发人员可以在本地快速搭建与生产环境相似的完整应用栈,方便进行开发和调试。修改代码后,通常只需简单的命令即可重建和重启服务。
- 声明式配置:YAML 文件提供了一种清晰、易读的方式来描述应用架构。这种声明式的方法让你专注于“需要什么”,而不是“如何一步步实现”。
- 易于协作和版本控制:
docker-compose.yml
文件可以像代码一样纳入版本控制系统(如 Git),方便团队成员共享、协作和追踪环境配置的变更历史。 - 集成 Docker 特性:Compose 无缝集成了 Docker 的核心功能,如网络管理(自动创建专用网络)、卷管理(数据持久化)以及构建镜像(通过
build
指令)。
2. 安装 Docker Compose
Docker Compose 的安装通常非常简单:
- Docker Desktop (Windows/macOS):如果你使用的是 Docker Desktop,它已经内置了 Docker Compose,无需额外安装。你可以通过在终端运行
docker-compose --version
或docker compose version
(注意:新版推荐使用docker compose
,集成到 Docker CLI 中) 来验证。 - Linux:
- 推荐方式 (集成 Docker CLI 插件):按照 Docker 官方文档安装
docker-compose-plugin
。这通常涉及下载二进制文件并放置到 Docker CLI 插件目录(如~/.docker/cli-plugins
或/usr/local/lib/docker/cli-plugins
)。之后可以使用docker compose
命令。 - 独立二进制文件 (旧版):也可以从 Docker Compose 的 GitHub Releases 页面下载独立的
docker-compose
二进制文件,赋予执行权限,并将其移动到系统路径下(如/usr/local/bin/docker-compose
)。这种方式使用docker-compose
命令。
- 推荐方式 (集成 Docker CLI 插件):按照 Docker 官方文档安装
请参考 Docker 官方文档获取针对你操作系统的最新、最准确的安装指南。
3. Docker Compose 的核心:docker-compose.yml
文件
docker-compose.yml
文件是 Docker Compose 的灵魂。它使用 YAML (YAML Ain’t Markup Language) 格式定义应用程序的服务、网络和卷。YAML 是一种易于人类阅读的数据序列化标准,对缩进非常敏感(通常使用两个空格进行缩进)。
一个典型的 docker-compose.yml
文件结构如下:
“`yaml
可选:指定 Compose 文件格式版本,推荐使用较新版本以利用新特性
version: ‘3.8’ # 或者 ‘3.9’, ‘2.4’ 等
定义应用程序包含的服务(容器)
services:
# 服务名称,可自定义,如 ‘web’, ‘db’, ‘api’ 等
webapp:
# 指定服务使用的镜像
image: nginx:alpine # 可以是 Docker Hub 上的镜像,或私有仓库镜像
# 或者,指定 Dockerfile 的路径来构建镜像
# build: . # 构建当前目录下的 Dockerfile
# build:
# context: ./app # Dockerfile 所在的上下文路径
# dockerfile: Dockerfile-dev # 指定 Dockerfile 文件名
# 端口映射 <宿主机端口>:<容器端口>
ports:
- "8080:80"
# 卷挂载,用于数据持久化或代码同步
volumes:
# 匿名卷 (由 Docker 管理)
# - /usr/share/nginx/html
# 命名卷 (推荐,更易于管理)
- webdata:/usr/share/nginx/html
# 绑定挂载 (将宿主机路径映射到容器内)
# - ./html:/usr/share/nginx/html:ro # ro 表示只读
# 环境变量
environment:
- NGINX_HOST=myapp.local
- NGINX_PORT=80
# 也可以使用字典格式
# NGINX_SETTINGS: "some_value"
# 网络配置,默认会创建一个默认网络
# networks:
# - app_network
# 定义服务间的启动依赖关系
depends_on:
- db # webapp 会在 db 服务启动后才启动
# 覆盖容器默认的启动命令
# command: ["nginx", "-g", "daemon off;"]
# 重启策略
restart: unless-stopped # (no | always | on-failure | unless-stopped)
db:
image: postgres:14-alpine
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
– dbdata:/var/lib/postgresql/data # 使用命名卷持久化数据库数据
# networks:
# – app_network
restart: always
(可选) 定义命名卷
volumes:
webdata: # 与 service.volumes 中对应的命名卷
dbdata:
(可选) 定义自定义网络
networks:
app_network:
driver: bridge # 或其他网络驱动
“`
下面详细解释 docker-compose.yml
中的关键指令:
version
:指定 Compose 文件语法的版本。不同版本支持的指令和语法可能有所不同。推荐使用3.x
系列的较新版本。官方文档会列出各版本特性。-
services
:这是文件的核心部分,用于定义构成应用程序的各个服务(容器)。每个服务都在services
下有一个顶级键,这个键就是服务的名称(如webapp
,db
)。image: <image_name>:<tag>
:指定服务使用的 Docker 镜像。可以是 Docker Hub 上的公共镜像,也可以是私有仓库的镜像。build: <context_path>
或build: { context: <path>, dockerfile: <filename>, args: ... }
:如果服务需要基于 Dockerfile 构建镜像,则使用build
指令。context
: 指定 Dockerfile 所在的目录(构建上下文)。dockerfile
: 指定 Dockerfile 的文件名(如果不是默认的Dockerfile
)。args
: 定义构建时变量。
ports: ["<HOST_PORT>:<CONTAINER_PORT>"]
:将宿主机的端口映射到容器的端口。例如"8080:80"
表示访问宿主机的 8080 端口会转发到容器的 80 端口。volumes: ["<SOURCE>:<TARGET>:<MODE>"]
:定义卷挂载,用于数据持久化或共享文件。- 命名卷 (Named Volumes):如
mydata:/app/data
。mydata
是卷的名称,在文件末尾的volumes:
部分定义。这是推荐的数据持久化方式,由 Docker 管理,更易于备份和迁移。 - 绑定挂载 (Bind Mounts):如
./src:/app/src
。将宿主机的路径 (./src
) 挂载到容器内的路径 (/app/src
)。常用于开发环境,可以直接在宿主机修改代码并立即在容器中生效。 MODE
(可选):ro
(只读),rw
(读写,默认)。
- 命名卷 (Named Volumes):如
environment: ["<KEY>=<VALUE>"]
或environment: { <KEY>: <VALUE> }
:设置容器内的环境变量。这对于传递配置信息(如数据库密码、API 密钥)非常有用。也可以使用.env
文件来管理环境变量(后面会提到)。networks: ["<network_name>"]
:将服务连接到指定的网络。默认情况下,Compose 会为所有服务创建一个默认的桥接网络,服务可以通过服务名称相互访问(例如,webapp
可以通过http://db:5432
访问db
服务)。如果需要更复杂的网络拓扑,可以定义自定义网络。depends_on: ["<service_name>"]
:定义服务间的启动依赖关系。Compose 会按照依赖顺序启动服务。例如,webapp
设置了depends_on: [db]
,则db
服务会先于webapp
启动。注意:这只保证db
容器已启动,不保证db
服务内部的应用程序已完全准备好接受连接。对于需要等待服务就绪的情况,可能需要额外的健康检查或等待脚本。command: <command>
或command: ["executable", "param1", "param2"]
:覆盖容器镜像中定义的默认启动命令 (CMD)。entrypoint: <command>
或entrypoint: ["executable", "param1", "param2"]
:覆盖容器镜像中定义的默认入口点 (ENTRYPOINT)。restart: <policy>
:定义容器退出时的重启策略。常用策略包括:no
: 不重启(默认)。always
: 总是重启,无论退出状态码是什么。on-failure
: 仅在退出状态码非 0 时重启。可以指定最大尝试次数on-failure:5
。unless-stopped
: 总是重启,除非容器被手动停止 (e.g.,docker stop
,docker-compose stop
)。
expose: ["<PORT>"]
:仅暴露端口给同一网络内的其他服务,而不映射到宿主机。healthcheck: { test: [...], interval: ..., timeout: ..., retries: ..., start_period: ... }
:定义如何检查服务是否健康。depends_on
可以结合condition: service_healthy
来等待依赖服务真正就绪。
-
volumes
:在文件顶层定义命名卷。在这里定义的卷可以在services
部分通过名称引用。
yaml
volumes:
db_data: # 定义一个名为 db_data 的卷
app_config:
driver: local # 可以指定卷驱动及选项 networks
:在文件顶层定义自定义网络。可以在services
部分将服务连接到这些网络。
yaml
networks:
frontend:
driver: bridge
backend:
driver: bridge
ipam: # 可以配置 IP 地址管理
config:
- subnet: 172.16.238.0/24
4. 常用 Docker Compose 命令
掌握了 docker-compose.yml
的编写,接下来需要学习如何使用 docker-compose
(或 docker compose
) 命令来管理应用。假设你的 docker-compose.yml
文件位于当前目录下。
-
docker-compose up
: 这是最常用的命令。它会根据docker-compose.yml
文件创建(或重建)并启动所有服务。- 默认情况下,它会在前台运行,并将所有服务的日志输出到终端。按
Ctrl+C
会停止并移除容器。 docker-compose up -d
: 在后台(Detached mode)启动服务。这是生产或长时间运行服务的常用方式。docker-compose up --build
: 在启动服务前强制重新构建镜像(即使镜像已存在)。docker-compose up <service_name>
: 只启动指定的服务及其依赖项。
- 默认情况下,它会在前台运行,并将所有服务的日志输出到终端。按
-
docker-compose down
: 停止并移除由up
命令创建的容器、网络。docker-compose down -v
: 同时移除与服务关联的命名卷(注意:这会删除持久化数据!)。docker-compose down --rmi all
: 移除容器的同时,移除所有在docker-compose.yml
中定义的镜像。docker-compose down --rmi local
: 只移除没有自定义标签的镜像。
-
docker-compose ps
: 列出当前 Compose 应用中正在运行的容器及其状态。 -
docker-compose logs
: 查看服务的日志输出。docker-compose logs -f
: 持续跟踪(follow)日志输出。docker-compose logs <service_name>
: 只查看指定服务的日志。docker-compose logs --tail="50"
: 只显示最后 50 行日志。
-
docker-compose build
: 构建(或重新构建)docker-compose.yml
文件中定义的服务镜像。docker-compose build <service_name>
: 只构建指定服务的镜像。docker-compose build --no-cache
: 构建镜像时不使用缓存。
-
docker-compose pull
: 拉取docker-compose.yml
文件中定义的服务所需的镜像。docker-compose pull <service_name>
: 只拉取指定服务的镜像。
-
docker-compose stop
: 停止正在运行的服务容器,但不移除它们。可以使用docker-compose start
重新启动。 -
docker-compose start
: 启动已存在的、被停止的服务容器。 -
docker-compose restart
: 重启服务容器。 -
docker-compose exec <service_name> <command>
: 在指定的、正在运行的服务容器内部执行一个命令。非常适合进行调试或执行一次性任务。- 例如:
docker-compose exec webapp bash
会在webapp
容器内启动一个交互式 Bash shell。
- 例如:
-
docker-compose run <service_name> <command>
: 为指定的服务创建一个新的一次性容器并执行命令。它不会启动服务依赖项(除非指定--service-ports
或--use-aliases
)。常用于运行数据库迁移、测试脚本等。- 例如:
docker-compose run --rm webapp python manage.py migrate
(假设webapp
是 Django 应用,--rm
表示命令执行后移除容器)。
- 例如:
-
docker-compose config
: 验证并查看 Compose 文件的最终配置(合并了.env
文件、覆盖文件等)。docker-compose config --services
: 只列出服务名称。docker-compose config --volumes
: 只列出卷名称。
-
docker-compose -f <file_name.yml> <command>
: 使用指定的 Compose 文件(而不是默认的docker-compose.yml
)。可以指定多个-f
来合并多个文件(例如,基础配置 + 开发环境覆盖配置)。 -
docker-compose --project-name <name> <command>
或docker-compose -p <name> <command>
: 指定一个项目名称。Compose 会使用项目名称作为创建的容器、网络和卷的前缀,以避免不同项目间的命名冲突。默认项目名称是包含docker-compose.yml
的目录名。
注意: 新版本的 Docker (通常 20.10+) 推荐使用 docker compose
(无连字符) 命令,它作为 Docker CLI 的一部分提供。其用法与 docker-compose
基本兼容,并可能提供更好的集成和性能。如果你的系统同时存在两者,建议优先使用 docker compose
。例如,docker compose up -d
, docker compose down
。
5. 实战示例:一个简单的 Web 应用 (Python Flask + Redis)
让我们通过一个实例来巩固所学知识。我们将创建一个简单的 Web 应用:一个 Python Flask 应用,它会记录并显示页面的访问次数,并将计数器存储在 Redis 缓存中。
项目结构:
my_compose_app/
├── docker-compose.yml
├── app/
│ ├── app.py
│ └── requirements.txt
└── Dockerfile # Flask 应用的 Dockerfile
1. app/requirements.txt
: (Flask 应用的依赖)
txt
flask
redis
2. app/app.py
: (简单的 Flask 应用)
“`python
from flask import Flask
import redis
import os
import socket
app = Flask(name)
连接到 Redis 服务,’redis’ 是 docker-compose.yml 中定义的服务名
Docker Compose 会自动处理 DNS 解析,让 ‘redis’ 指向 Redis 容器的 IP
redis_host = os.environ.get(‘REDIS_HOST’, ‘redis’) # 从环境变量读取 Redis 主机名,默认为 ‘redis’
redis_port = int(os.environ.get(‘REDIS_PORT’, 6379)) # 从环境变量读取 Redis 端口,默认为 6379
try:
# 使用 decode_responses=True 让 Redis 返回字符串而不是字节
cache = redis.Redis(host=redis_host, port=redis_port, db=0, decode_responses=True)
cache.ping() # 测试连接
app.logger.info(f”Successfully connected to Redis at {redis_host}:{redis_port}”)
except redis.exceptions.ConnectionError as e:
app.logger.error(f”Could not connect to Redis: {e}”)
# 在无法连接 Redis 时提供一个降级方案或明确的错误处理
cache = None # 标记 Redis 不可用
@app.route(‘/’)
def hello():
hostname = socket.gethostname()
if cache:
try:
# 使用 incr 原子地增加 ‘hits’ 键的值
count = cache.incr(‘hits’)
return f’
Hello from Docker Compose!
I am running on container: {hostname}
This page has been viewed {count} times.
‘
except redis.exceptions.ConnectionError as e:
app.logger.error(f”Redis connection error during request: {e}”)
return f’
Hello from Docker Compose!
I am running on container: {hostname}
Error connecting to Redis counter.
‘, 500
else:
return f’
Hello from Docker Compose!
I am running on container: {hostname}
Redis is not available.
‘, 500
if name == “main“:
# 监听所有网络接口,端口 5000
app.run(host=”0.0.0.0”, port=5000, debug=True)
“`
3. Dockerfile
: (用于构建 Flask 应用镜像)
“`dockerfile
使用官方 Python 基础镜像
FROM python:3.9-slim
设置工作目录
WORKDIR /app
复制依赖文件
COPY app/requirements.txt requirements.txt
安装依赖
RUN pip install –no-cache-dir -r requirements.txt
复制应用代码到工作目录
COPY app/ .
暴露 Flask 应用运行的端口
EXPOSE 5000
容器启动时运行的命令
CMD [“python”, “app.py”]
“`
4. docker-compose.yml
: (定义服务)
“`yaml
version: ‘3.8’
services:
# Web 应用服务
web:
# 使用当前目录下的 Dockerfile 进行构建
build: .
# 将宿主机的 8000 端口映射到容器的 5000 端口
ports:
– “8000:5000”
# 将当前目录下的 app 文件夹挂载到容器的 /app 目录
# 这使得在宿主机修改代码后,无需重建镜像即可生效(适用于开发)
volumes:
– ./app:/app
# 设置环境变量,传递给 Flask 应用
environment:
– FLASK_ENV=development # 设置 Flask 环境为开发模式
– REDIS_HOST=redis # 指定 Redis 服务的主机名
– REDIS_PORT=6379 # 指定 Redis 服务的端口
# 声明依赖于 redis 服务,确保 redis 先启动
depends_on:
– redis
# 连接到 backend 网络
networks:
– backend
# Redis 缓存服务
redis:
# 使用官方 Redis 镜像
image: “redis:alpine”
# 将 Redis 数据持久化到命名卷 redis_data
volumes:
– redis_data:/data
# 连接到 backend 网络
networks:
– backend
# 总是重启 Redis 服务,除非手动停止
restart: unless-stopped
定义命名卷
volumes:
redis_data: # 用于持久化 Redis 数据
定义自定义网络
networks:
backend: # 所有服务都连接到这个网络,可以通过服务名互相访问
driver: bridge
“`
运行应用:
- 在
my_compose_app
目录下打开终端。 - 运行命令启动应用:
bash
docker compose up -d --build
# 或者使用旧版命令: docker-compose up -d --build--build
确保会根据Dockerfile
构建web
服务的镜像。-d
让应用在后台运行。
- 等待命令执行完成。Compose 会:
- 构建
web
镜像(如果需要)。 - 拉取
redis
镜像(如果本地没有)。 - 创建
backend
网络。 - 创建
redis_data
命名卷。 - 按照依赖顺序启动
redis
和web
容器。
- 构建
- 打开浏览器,访问
http://localhost:8000
。 - 每次刷新页面,你应该能看到访问次数增加,这表明 Flask 应用成功连接到 Redis 并更新了计数器。计数器数据因为使用了命名卷,即使停止并重新启动应用 (
docker compose down
后再docker compose up
),计数也会保留。
管理应用:
- 查看运行状态:
docker compose ps
- 查看日志:
docker compose logs -f web
或docker compose logs redis
- 停止并移除容器、网络:
docker compose down
- 停止并移除容器、网络及数据卷(小心!):
docker compose down -v
这个例子展示了 Docker Compose 如何协调多个服务(Web 应用和数据库/缓存),管理它们的构建、网络、数据持久化和依赖关系,极大地简化了多容器应用的开发和部署流程。
6. 进阶技巧与最佳实践
-
使用
.env
文件管理环境变量: 为了避免将敏感信息(如密码、API Key)直接写入docker-compose.yml
并提交到版本控制,可以在docker-compose.yml
同级目录下创建一个.env
文件,Compose 会自动加载它。.env
文件内容 (示例):
env
POSTGRES_PASSWORD=supersecret
API_KEY=xyz123abc
REDIS_PORT=6380- 在
docker-compose.yml
中引用:
yaml
services:
db:
image: postgres:14
environment:
# 会自动从 .env 文件读取 POSTGRES_PASSWORD
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: user
POSTGRES_DB: myapp
api:
image: myapi
environment:
API_KEY: ${API_KEY} # 从 .env 读取
# 也可以提供默认值
CACHE_PORT: ${REDIS_PORT:-6379} # 如果 .env 中没定义 REDIS_PORT,则使用 6379 - 将
.env
文件添加到.gitignore
中,防止敏感信息泄露。
-
多环境配置 (Override Files): 通常开发、测试、生产环境的配置会有所不同(例如端口映射、卷挂载方式、资源限制等)。可以使用多个 Compose 文件来实现:
docker-compose.yml
: 存放基础、通用的配置。docker-compose.override.yml
: 存放开发环境的特定配置(Compose 默认会加载此文件并合并)。docker-compose.prod.yml
: 存放生产环境的特定配置。-
运行时通过
-f
参数指定:
“`bash
# 开发环境 (自动加载 docker-compose.yml 和 docker-compose.override.yml)
docker compose up -d生产环境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
“`
-
优化镜像构建:
- 利用构建缓存:合理组织 Dockerfile 的指令顺序,将不经常变动的部分(如安装系统依赖)放在前面。
- 使用
.dockerignore
文件排除不需要复制到镜像中的文件(如.git
,node_modules
, 临时文件),减小构建上下文和镜像体积。 - 考虑多阶段构建 (Multi-stage builds) 来创建更小的生产镜像。
-
健康检查 (Healthchecks): 在
docker-compose.yml
中为关键服务(如数据库、API)定义healthcheck
,并结合depends_on
的condition: service_healthy
,确保依赖的服务不仅已启动,而且真正准备好接受连接。 -
资源限制: 在
deploy
配置块下(需要较新 Compose 版本)可以设置服务的资源限制(CPU、内存),防止某个服务耗尽系统资源。
yaml
services:
web:
image: myapp
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
注意:deploy
配置通常在 Swarm 模式下完全生效,但在单机 Compose (v2.1+) 中部分资源限制也可能被支持。 -
理解网络: 默认情况下,Compose 创建的桥接网络提供了服务间的 DNS 解析(通过服务名访问)。了解不同网络驱动(bridge, host, overlay 等)及其适用场景有助于构建更复杂的应用。
7. 总结
Docker Compose 是 Docker 生态系统中一个极其重要且实用的工具。它通过一个简洁的 docker-compose.yml
文件,将复杂的多容器应用配置化、标准化,极大地简化了应用的定义、启动、停止和管理流程。无论是本地开发测试,还是持续集成/持续部署 (CI/CD) 流程,乃至简单的生产环境部署,Docker Compose 都能显著提高效率和一致性。
掌握 Docker Compose 的基础知识——理解 docker-compose.yml
的核心指令、熟练运用常用命令、并通过实践来构建多服务应用——是每一位使用 Docker 的开发人员和运维工程师的必备技能。希望本指南能为你打下坚实的基础,助你更从容地驾驭容器化的世界。随着你经验的增长,可以进一步探索 Compose 的高级特性,如 Profiles、扩展 (extends)、与 Docker Swarm 或 Kubernetes 的集成等,不断提升应用部署和管理的水平。