导语:我们已经成功地在本地开发并运行了“旅小智”这个由前端、后端和 AI 核心组成的全栈应用。但是,我们的“征途”还未结束。如何将这个由多个服务组成的复杂系统,方便、可靠地部署到任何地方?如何让一个新同事仅用一个命令就将整个应用跑起来?答案,就在于 Docker Compose。在本章中,我们将学习如何为“旅小智”的每个部分(FastAPI 后端、Streamlit 前端)分别编写 Dockerfile,然后使用 Docker Compose 这根“魔法棒”,将它们编排成一个有机的、一键启动的整体。这将是你从部署单个容器到部署完整微服务应用的决定性一步。
目录
- 从单个容器到多容器应用:为什么需要 Docker Compose?
- 回顾:
docker run的局限性 - Docker Compose 的角色:多容器应用的“总指挥”
docker-compose.yml:定义你的应用“天团”
- 回顾:
- 第一步:为每个服务创建独立的 Dockerfile
app/Dockerfile:后端 FastAPI 服务的容器化清单ui/Dockerfile:前端 Streamlit 服务的容器化清单- 保持 Dockerfile 的简洁与专注
- 第二步:编写
docker-compose.ymlservices: 定义应用的组成部分(backend,frontend)build: 指定每个服务对应的 Dockerfile 路径ports: 将容器端口映射到宿主机volumes: 实现代码热重载与数据持久化environment/env_file: 管理敏感信息和环境变量depends_on: 定义服务间的启动依赖关系networks: 创建一个共享网络,让容器间通过“服务名”通信
- 第三步:网络与通信的关键
- Compose 网络:
backend服务如何被frontend服务找到? - 修改 Streamlit 代码:将 API 地址从
localhost改为后端的服务名(http://backend:8000)
- Compose 网络:
- 第四步:一键启动与管理
docker-compose up --build: 构建并启动你的全栈 AI 应用docker-compose down: 停止并移除所有相关容器和网络- 查看日志:
docker-compose logs -f <service_name>
- 实战演练:部署“旅小智”
- 将所有文件放在正确的位置
- 运行
docker-compose up,见证奇迹 - 在浏览器中访问 Streamlit UI,并验证其与后端容器的通信
- 总结:迈向专业的微服务部署
1. 从单个容器到多容器应用:为什么需要 Docker Compose?
在 2.6 节中,我们学会了用 docker build 和 docker run 来部署单个服务。对于 DeepResearch 项目,这已经足够了。但“旅小智”是一个多服务应用,它至少包含两个需要同时运行的进程:
- FastAPI 后端服务。
- Streamlit 前端服务。
我们可以手动管理它们:
- 打开终端 A,运行
docker run ...启动后端容器。 - 打开终端 B,运行
docker run ...启动前端容器。 - 手动创建一个 Docker 网络,让它们可以互相通信。
- ...
这个过程非常繁琐、易错,并且难以自动化。
Docker Compose 的角色:多容器应用的“总指挥”
Docker Compose 是 Docker 官方提供的、用于定义和运行多容器 Docker 应用的工具。它允许你使用一个 YAML 文件(docker-compose.yml)来配置应用的所有服务,然后用一个简单的命令,就可以根据你的配置创建和启动所有服务。
它就像一个“总指挥”,你把所有“兵种”(服务)的配置都写在一张“作战计划”(docker-compose.yml)上,然后一声令下(docker-compose up),所有部队都能协同作战。
2. 第一步:为每个服务创建独立的 Dockerfile
我们的项目结构已经为这一步做好了准备。我们需要在 app/ 和 ui/ 目录下分别创建 Dockerfile。
app/Dockerfile
这个文件与我们在 2.6 节中为 FastAPI 服务编写的 Dockerfile 基本相同。
# trip-genius/app/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY ./app /app
# 注意:这里我们假设 requirements.txt 在 app 目录下
RUN pip install --no-cache-dir -r requirements.txt
# 端口声明
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
ui/Dockerfile
前端服务的 Dockerfile也非常类似,只是启动命令和暴露的端口不同。
# trip-genius/ui/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY ./ui /app
# 注意:这里我们假设 requirements.txt 在 ui 目录下
RUN pip install --no-cache-dir -r requirements.txt
# Streamlit 默认端口是 8501
EXPOSE 8501
# 启动命令
CMD ["streamlit", "run", "chat_app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]
3. 第二步:编写 docker-compose.yml
这是本章的核心。在项目的根目录(trip-genius/)下,创建 docker-compose.yml 文件。
# trip-genius/docker-compose.yml
version: '3.8' # 指定 compose 文件版本
services:
# --- 后端服务定义 ---
backend:
build:
context: . # Dockerfile 的上下文路径
dockerfile: app/Dockerfile # 指定 Dockerfile 的位置
ports:
- "8000:8000" # 将宿主机的 8000 端口映射到容器的 8000 端口
env_file:
- .env # 从 .env 文件加载环境变量
volumes:
- ./app:/app # 将本地 app 目录挂载到容器的 /app 目录,实现代码热重载
- ./agents:/app/agents # 同样挂载 agents 目录
- sqlite_data:/app/data # 将 sqlite 数据库文件持久化到名为 sqlite_data 的卷中
networks:
- trip_genius_net # 将服务连接到我们自定义的网络
# --- 前端服务定义 ---
frontend:
build:
context: .
dockerfile: ui/Dockerfile
ports:
- "8501:8501" # 映射 Streamlit 的端口
volumes:
- ./ui:/app # 实现前端代码的热重载
depends_on:
- backend # 确保后端服务启动后,再启动前端
networks:
- trip_genius_net
# --- 网络定义 ---
networks:
trip_genius_net:
driver: bridge # 使用默认的桥接网络驱动
# --- 卷定义 ---
volumes:
sqlite_data: # 定义一个命名的卷,用于持久化数据库
docker-compose.yml 指令解读:
services: 定义了组成我们应用的两个服务:backend和frontend。build.context: 设置为.(根目录),这样 Dockerfile 在构建时就可以访问到项目的所有文件(比如从app/目录COPY ./agents)。build.dockerfile: 精确指定了每个服务使用的 Dockerfile。ports: 和docker run -p一样,负责端口映射,让我们可以从宿主机访问容器内的服务。env_file: 从根目录下的.env文件加载环境变量,并注入到backend容器中。volumes: 这是开发时的一大利器。./app:/app将我们本地的app目录“覆盖”到容器的/app目录。这意味着当我们在本地修改了main.py,容器内的文件会实时同步,uvicorn的热重载会立即生效,我们无需重新构建镜像就能看到改动。sqlite_data:/app/data创建一个命名的卷(named volume)。Docker 会管理这个卷,确保即使容器被删除,卷中的数据(我们的数据库文件)也能保留下来,实现了数据持久化。
depends_on: 定义了服务间的依赖关系。这里表示frontend服务会等待backend服务启动完成后再启动,避免了前端启动时后端还未就绪的错误。networks: 这是实现容器间通信的关键。我们创建了一个名为trip_genius_net的自定义网络。所有加入这个网络的服务,都可以通过它们的服务名(backend,frontend)作为主机名(hostname)来互相访问。
4. 第三步:网络与通信的关键
Docker Compose 最神奇的地方之一就是它内置的 DNS 服务。对于 frontend 容器来说,主机名 backend 会被自动解析为 backend 容器的内部 IP 地址。
因此,我们必须修改前端代码,使其不再访问 localhost。
修改 ui/chat_app.py
# ui/chat_app.py
# ...
import os
# --- 后端 API 地址 ---
# 不再硬编码,而是从环境变量读取,如果不存在则默认为本地开发地址
# 这使得我们的代码更具灵活性
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000")
# ...
# 在调用 requests 时,使用这个变量
# with requests.post(f"{BACKEND_URL}/invoke", ...)
# ...
现在,我们还需要在 docker-compose.yml 中为 frontend 服务设置这个环境变量。
修改 docker-compose.yml
# ...
services:
backend:
# ...
frontend:
# ...
environment:
- BACKEND_URL=http://backend:8000 # 关键!
# ...
现在,当 frontend 容器启动时,它内部的 chat_app.py 代码会读取到 BACKEND_URL 这个环境变量,其值为 http://backend:8000。当它发起 requests 调用时,就会正确地访问到同一网络下的 backend 容器的 8000 端口。
5. 第四步:一键启动与管理
现在,所有的配置都已就绪。
启动应用
在项目根目录(trip-genius/)下,打开一个终端,运行:
docker-compose up --build
docker-compose up: 根据docker-compose.yml文件启动并运行所有服务。--build: 在启动前,强制重新构建镜像。第一次运行时需要,后续如果 Dockerfile 没有改变则可以省略。- (可以加上
-d在后台运行)
你会看到 Docker Compose 依次构建 backend 和 frontend 的镜像,然后按顺序启动容器。两个服务的日志会交错地显示在同一个终端中,非常便于观察整体情况。
停止应用
当你想关闭整个应用时,在同一个终端按下 Ctrl+C,或者如果是在后台运行,则执行:
docker-compose down
这个命令会优雅地停止并移除所有相关的容器和网络,让你的系统恢复干净。
查看特定服务的日志
如果日志太多太乱,你可以只看某个服务的日志:
docker-compose logs -f frontend
6. 实战演练:部署“旅小智”
- 确保你的项目文件结构与设计的一致。
- 确保
app/Dockerfile,ui/Dockerfile,docker-compose.yml的内容正确无误。 - 在根目录创建
.env文件并填入你的 API keys。 - 运行
docker-compose up --build。 - 等待所有服务启动成功。
- 在浏览器中打开
http://localhost:8501。 - 开始与“旅小智”对话,观察终端中
backend和frontend服务的日志,验证它们之间的通信和 AI 逻辑是否正常工作。
7. 总结:迈向专业的微服务部署
通过本章,你已经掌握了使用 Docker Compose 部署一个完整、多服务 AI 应用的“屠龙之技”。
你不再将应用视为一个单一的、巨大的整体,而是学会了将其拆分为多个职责单一的微服务(backend, frontend),并使用 Docker Compose 这个强大的工具来编排它们。
这不仅仅是一种部署技巧,更是一种现代化的软件架构思想。它让你的应用:
- 更易于维护:你可以独立地更新或修改某个服务,而无需触动整个系统。
- 更具扩展性:未来当后端压力过大时,你可以轻易地通过
docker-compose up --scale backend=3来水平扩展后端服务的实例数量。 - 更贴近生产:几乎所有的现代云原生应用都是以类似的方式进行组织和部署的。
你已经为将你的 AI 应用部署到真实的生产云环境(如 Kubernetes)打下了最坚实的基础。