Docker Compose 快速入门
- [Docker Compose 快速入门](#Docker Compose 快速入门)
-
- 前提条件
- [第 1 步:搭建项目](#第 1 步:搭建项目)
- [第 2 步:定义并启动你的服务](#第 2 步:定义并启动你的服务)
- [第 3 步:通过健康检查修复启动竞态](#第 3 步:通过健康检查修复启动竞态)
- [第 4 步:启用 Compose Watch 实现实时更新](#第 4 步:启用 Compose Watch 实现实时更新)
- [第 5 步:使用命名卷持久化数据](#第 5 步:使用命名卷持久化数据)
- [第 6 步:使用多个 Compose 文件组织项目](#第 6 步:使用多个 Compose 文件组织项目)
- [第 7 步:检查和调试正在运行的栈](#第 7 步:检查和调试正在运行的栈)
-
- 流式查看所有服务的日志
- 在正在运行的容器内运行命令
-
- 验证环境变量设置正确
- [测试 `web` 容器能否使用服务名称作为主机名访问 Redis](#测试
web容器能否使用服务名称作为主机名访问 Redis) - [检查 Redis 中访问计数器的实时值](#检查 Redis 中访问计数器的实时值)
Docker Compose 快速入门
本教程旨在通过引导你开发一个基础的 Python Web 应用,来介绍 Docker Compose 的基本概念。
该应用使用 Flask 框架,并借助 Redis 实现了一个访问计数器,为 Docker Compose 在 Web 开发场景中的应用提供了一个实用的示例。即使你不熟悉 Python,这里展示的概念也应该容易理解。
前提条件
请确保你已经:
- 安装了最新版本的 Docker Compose
- 对 Docker 概念以及 Docker 的工作原理有基本了解
第 1 步:搭建项目
-
为项目创建一个目录:
console$ mkdir compose-demo $ cd compose-demo -
在项目目录中创建
app.py,并添加以下内容:pythonimport os import redis from flask import Flask app = Flask(__name__) cache = redis.Redis( host=os.getenv("REDIS_HOST", "redis"), port=int(os.getenv("REDIS_PORT", "6379")), ) @app.route("/") def hello(): count = cache.incr("hits") return f"Hello from Docker! I have been seen {count} time(s).\n"该应用从环境变量中读取 Redis 连接信息,并提供了合理的默认值,因此开箱即用。
-
在项目目录中创建
requirements.txt,并添加以下内容:textflask redis -
创建一个
Dockerfile:dockerfile# syntax=docker/dockerfile:1 # 基于 Python 3.12 镜像构建 FROM python:3.12-alpine # 将工作目录设置为 `/code` WORKDIR /code # 设置 `flask` 命令使用的环境变量 ENV FLASK_APP=app.py ENV FLASK_RUN_HOST=0.0.0.0 # 安装 `gcc` 及其他依赖 RUN apk add --no-cache gcc musl-dev linux-headers # 复制 `requirements.txt` COPY requirements.txt . # 安装 Python 依赖 RUN pip install -r requirements.txt # 将项目中的当前目录 `.` 复制到镜像中的工作目录 `.` COPY . . EXPOSE 5000 # 将容器的默认命令设置为 `flask run --debug` CMD ["flask", "run", "--debug"]!IMPORTANT
请确保文件名为
Dockerfile,没有扩展名。有些编辑器会自动添加.txt,这会导致构建失败。
有关如何编写 Dockerfile 的更多信息,请参阅 Dockerfile 参考。
-
创建一个
.env文件来保存配置值:textAPP_PORT=8000 REDIS_HOST=redis REDIS_PORT=6379Compose 会自动读取
.env,并让这些值可用于compose.yaml中的变量插值。对于这个示例来说,好处不算大,但在实践中,将配置排除在 Compose 文件之外可以更容易地:
- 在不同环境之间更改值而无需编辑 YAML
- 避免将机密提交到版本控制
- 在多个服务之间复用值
-
创建一个
.dockerignore文件,将不必要的文件排除在构建上下文之外:text.env *.pyc __pycache__ redis-dataDocker 在构建镜像时会把你项目目录中的所有内容发送给守护进程。
如果没有
.dockerignore,其中就会包含你的.env文件(可能含有机密)以及任何缓存的 Python 字节码。排除它们可以保持构建速度,并避免意外
将敏感值固化到镜像层中。
第 2 步:定义并启动你的服务
Compose 简化了整个应用栈的控制,让你可以在单个 YAML 配置文件中轻松管理服务、网络和卷。
-
在项目目录中创建
compose.yaml,并粘贴以下内容:yamlservices: web: build: . ports: - "${APP_PORT}:5000" environment: - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} redis: image: redis:alpine这个 Compose 文件定义了两个服务:
-
web服务使用基于当前目录中的Dockerfile构建的镜像。它将主机上的端口8000映射到容器中 Flask 默认监听的端口5000。 -
redis服务使用从 Docker Hub 注册表拉取的公共 Redis 镜像。
有关
compose.yaml文件的更多信息,请参阅 Compose 如何工作。 -
-
启动你的应用:
console$ docker compose up只需一条命令,你就可以根据配置文件创建并启动所有服务。Compose 会构建你的 web 镜像、拉取 Redis 镜像,并启动两个容器。
-
打开
http://localhost:8000。你应该会看到:textHello from Docker! I have been seen 1 time(s).刷新页面------每次访问计数器都会递增。
这个最小化设置可以工作,但它有两个问题,你将在接下来的步骤中修复:
- 启动竞态:
web与redis同时启动。如果 Redis 还没准备好,
Flask 应用会连接失败并崩溃。 - 没有持久化:如果你运行
docker compose down然后再运行docker compose up,
计数器会重置为零。docker compose down会删除容器,随之删除写入容器可写层的所有数据。
docker compose stop会保留容器,因此数据可以保留,但在生产环境中容器会定期被替换,
你不能依赖这一点。
- 启动竞态:
-
在继续之前停止整个栈:
console$ docker compose down
第 3 步:通过健康检查修复启动竞态
要修复启动竞态,Compose 需要等待 redis 被确认健康后,再启动 web。
-
更新
compose.yaml:yamlservices: web: build: . ports: - "${APP_PORT}:5000" environment: - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} depends_on: redis: condition: service_healthy redis: image: redis:alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 start_period: 10shealthcheck块告诉 Compose 如何测试 Redis 是否已准备好:test是 Compose 在容器内运行以检查其健康状况的命令。
redis-cli ping会连接到 Redis 并期望返回PONG响应------如果收到该响应,
容器就是健康的。start_period给 Redis 10 秒的初始化时间,之后才开始健康检查。
在这个时间窗口内的任何失败都不会计入重试次数。interval在启动期结束后每 5 秒运行一次检查。timeout给每次检查 3 秒的响应时间,超时则视为失败。retries设置 Compose 将容器标记为不健康之前允许的连续失败次数。
在interval: 5s和retries: 5的情况下,Compose 最多会等待 25 秒才放弃。
-
启动整个栈以确认顺序问题已修复:
console$ docker compose up你应该会看到类似以下内容:
text[+] Running 2/2 ✔ Container compose-demo-redis-1 Healthy 0.0s -
打开
http://localhost:8000确认应用仍在工作,然后在继续之前停止整个栈:console$ docker compose down
第 4 步:启用 Compose Watch 实现实时更新
如果没有 Compose Watch,每次代码更改都需要你停止整个栈、重新构建镜像并重启容器。
Compose Watch 通过在你保存文件时自动将更改同步到正在运行的容器中,消除了这个循环。
-
更新
compose.yaml,为web服务添加develop.watch块:yamlservices: web: build: . ports: - "${APP_PORT}:5000" environment: - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} depends_on: redis: condition: service_healthy develop: watch: - action: sync+restart path: . target: /code - action: rebuild path: requirements.txt redis: image: redis:alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 start_period: 10swatch块定义了两条规则:sync+restart操作会监视主机上的项目目录(.)。当文件发生变化时,
Compose 会将任何已更改的文件复制到正在运行的容器内的/code中,然后重启容器。
由于容器重启时更新后的文件已经就位,Flask 启动时会直接读取新代码------无需手动重新构建或重启。- 针对
requirements.txt的rebuild操作会在你添加新依赖时触发完整的镜像重新构建,
因为安装软件包需要重新构建镜像,而不仅仅是同步文件。
-
启用 Watch 启动整个栈:
console$ docker compose up --watch -
进行实时更改。打开
app.py并更新问候语:pythonreturn f"Hello from Compose Watch! I have been seen {count} time(s).\n" -
保存文件。Compose Watch 会检测到更改并立即同步:
textSyncing service "web" after changes were detected -
刷新
http://localhost:8000。更新后的问候语会出现,无需任何重启,计数器应该仍在递增。
-
在继续之前停止整个栈:
console$ docker compose down有关 Compose Watch 工作原理的更多信息,请参阅 使用 Compose Watch。
第 5 步:使用命名卷持久化数据
每次停止并重启整个栈时,访问计数器都会重置为零。Redis 数据
存放在容器内部,因此当容器被删除时数据也会消失。命名卷通过将数据存储在主机上、
容器生命周期之外来解决这个问题。
-
更新
compose.yaml:yamlservices: web: build: . ports: - "${APP_PORT}:5000" environment: - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} depends_on: redis: condition: service_healthy develop: watch: - action: sync+restart path: . target: /code - action: rebuild path: requirements.txt redis: image: redis:alpine volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 start_period: 10s volumes: redis-data:redis.volumes下的redis-data:/data条目将命名卷挂载到/data,这是 Redis 写入其数据文件的路径。顶层的
volumes键将其注册到 Docker,以便它在
compose down和compose up循环之间持久化。 -
使用
docker compose up --watch启动整个栈,并多次刷新http://localhost:8000以累积计数。 -
使用
docker compose down拆除整个栈,然后再次使用docker compose up --watch将其启动。 -
打开
http://localhost:8000------计数器会从上次中断的地方继续。 -
现在使用
docker compose down -v重置计数器。-v标志会连同容器一起删除命名卷。请有意使用它------它会永久删除存储的数据。
第 6 步:使用多个 Compose 文件组织项目
随着应用的增长,单个 compose.yaml 会变得越来越难以维护。include
顶层元素允许你将服务拆分到多个文件中,同时让它们仍属于同一个应用。
当不同团队负责栈的不同部分,或者你想在多个项目之间复用基础设施定义时,
这尤其有用。
-
在项目目录中创建一个名为
infra.yaml的新文件,并将 Redis 服务和卷移入其中:yamlservices: redis: image: redis:alpine volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 start_period: 10s volumes: redis-data: -
更新
compose.yaml以包含infra.yaml:yamlinclude: - path: ./infra.yaml services: web: build: . ports: - "${APP_PORT}:5000" environment: - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} depends_on: redis: condition: service_healthy develop: watch: - action: sync+restart path: . target: /code - action: rebuild path: requirements.txt -
运行应用以确认一切仍然正常:
console$ docker compose up --watchCompose 会在启动时合并两个文件。
web服务仍然可以通过名称引用redis,因为所有被包含的服务共享同一个默认网络。
这是一个简化的示例,但它展示了
include的基本原理,以及它如何更容易地将复杂应用模块化为子 Compose 文件。有关
include以及使用多个 Compose 文件的更多信息,请参阅 使用多个 Compose 文件。
-
在继续之前停止整个栈:
console$ docker compose down
第 7 步:检查和调试正在运行的栈
有了完整配置的栈,你可以在不停止任何东西的情况下观察容器内部发生的情况。
本步骤涵盖用于检查解析后的配置、流式查看日志以及在正在运行的容器内运行命令的核心命令。
在启动栈之前,验证 Compose 已解析你的 .env 变量并正确合并了所有文件:
console
$ docker compose config
docker compose config 不要求栈正在运行------它完全基于你的文件工作。输出中有几点值得注意:
${APP_PORT}、${REDIS_HOST}和${REDIS_PORT}都已替换为
你的.env文件中的值。- 短格式端口表示法(
"8000:5000")被展开为规范字段
(target、published、protocol)。 - 默认网络和卷名称被明确显示,并以项目名称
compose-demo为前缀。 - 输出是完全解析后的配置,通过
include引入的任何文件
都会合并到单个视图中。
当你想要确认 Compose 实际将应用什么时,尤其是在调试变量替换或使用多个 Compose 文件时,
可以随时使用 docker compose config。
现在以分离模式启动栈,以便终端可以腾出来执行后续命令:
console
$ docker compose up -d
流式查看所有服务的日志
console
$ docker compose logs -f
-f 标志会实时跟踪日志流,将两个容器的输出交错显示,
并带有按服务名称着色的前缀。多次刷新 http://localhost:8000,
观察 Flask 请求日志出现。要跟踪单个服务的日志,请传入其名称:
console
$ docker compose logs -f web
按 Ctrl+C 停止跟踪日志。容器会继续运行。
在正在运行的容器内运行命令
docker compose exec 在已经运行的容器内运行命令,而不会启动新容器。
这是进行实时调试的主要工具。
验证环境变量设置正确
console
$ docker compose exec web env | grep REDIS
text
REDIS_HOST=redis
REDIS_PORT=6379
测试 web 容器能否使用服务名称作为主机名访问 Redis
console
$ docker compose exec web python -c "import redis; r = redis.Redis(host='redis'); print(r.ping())"
text
True
这里使用了与你的应用相同的 redis 库,因此返回 True 确认了
服务发现、网络和 Redis 连接都端到端正常工作。
检查 Redis 中访问计数器的实时值
console
$ docker compose exec redis redis-cli GET hits