B站视频内容智能分析系统(二):Docker Compose 一键部署

系列文章目录

B站视频内容智能分析系统(一):项目介绍与架构设计

B站视频内容智能分析系统(二):Docker Compose 一键部署

文章目录

  • 系列文章目录
  • 前言
  • [一、为什么选 Docker Compose](#一、为什么选 Docker Compose)
  • 二、整体结构
  • [三、Profiles 区分环境](#三、Profiles 区分环境)
    • [1. 核心思路](#1. 核心思路)
    • [2. 两种环境对比](#2. 两种环境对比)
  • 四、服务拆解
    • [1. 基础设施:ChromaDB](#1. 基础设施:ChromaDB)
    • [2. Text-to-SQL 服务](#2. Text-to-SQL 服务)
    • [3. RAG 服务](#3. RAG 服务)
    • [4. Router Agent](#4. Router Agent)
    • [5. 前端 + Nginx](#5. 前端 + Nginx)
    • [6. GPU 转录服务(仅开发机)](#6. GPU 转录服务(仅开发机))
    • [7. bilibili-monitor(按需运行)](#7. bilibili-monitor(按需运行))
    • [8. bilibili-cron(定时调度)](#8. bilibili-cron(定时调度))
  • [五、Dockerfile 编写:国内镜像加速](#五、Dockerfile 编写:国内镜像加速)
    • [1. apt 换阿里云镜像](#1. apt 换阿里云镜像)
    • [2. pip 换清华 TUNA 镜像](#2. pip 换清华 TUNA 镜像)
    • [3. npm 换淘宝镜像](#3. npm 换淘宝镜像)
    • [4. 完整的 bilibili-monitor Dockerfile](#4. 完整的 bilibili-monitor Dockerfile)
  • [六、Volume 与数据共享](#六、Volume 与数据共享)
    • [1. 四个 Named Volume](#1. 四个 Named Volume)
    • [2. Bind Mount](#2. Bind Mount)
    • [3. Docker Socket 挂载](#3. Docker Socket 挂载)
  • 七、环境变量管理
    • [1. .env 文件结构](#1. .env 文件结构)
    • [2. 变量透传](#2. 变量透传)
    • [3. 默认值语法](#3. 默认值语法)
  • 八、健康检查与依赖关系
    • [1. healthcheck 配置](#1. healthcheck 配置)
    • [2. depends_on 启动顺序](#2. depends_on 启动顺序)
  • [九、Nginx 反向代理配置](#九、Nginx 反向代理配置)
  • 十、一键启动与验证
  • 总结

前言

上一篇讲了整体架构,这篇来讲怎么把它跑起来。

这个项目一共有 7 个 Docker 容器,如果一个个手动 docker run,光端口映射和环境变量就能搞半天。所以我用 Docker Compose 做统一编排,一行命令启动所有服务。

这篇会把 docker-compose.yml 从头到尾拆一遍,包括 profiles 怎么用、Dockerfile 怎么写国内镜像加速、Volume 怎么设计数据共享、健康检查和依赖顺序怎么配。

截图:docker compose up 启动过程,显示各容器依次创建和启动

一、为什么选 Docker Compose

先说为什么不用 Kubernetes。

理由很简单:单机部署,不需要 K8s 的复杂度

这个项目的生产环境是一台 NAS(Intel N150 + 8GB 内存),开发环境是一台笔记本电脑。都是单机,没有多节点的需求。Docker Compose 在这种场景下是最合适的选择:

  1. 一个 YAML 文件搞定所有容器:端口、环境变量、Volume、依赖关系全在里面
  2. profiles 区分环境:同一个文件,dev 和 nas 两套环境
  3. 一键启动/停止docker compose up -d 启动,docker compose down 停止
  4. 重建方便 :改了代码之后 docker compose up -d --build 就能重建镜像并重启

如果你的项目也是单机多容器,Docker Compose 基本就是最优解。

二、整体结构

先看一下整个 docker-compose.yml 的结构(不含具体配置,只看骨架):

yaml 复制代码
services:
  # ===== 基础设施 =====
  chromadb:          # 向量数据库
    profiles: [dev, nas]

  # ===== 核心业务 =====
  text-to-sql:       # 结构化查询
    profiles: [dev, nas]
  rag:               # 语义检索
    profiles: [dev, nas]
  router-agent:      # 路由分发
    profiles: [dev, nas]

  # ===== 前端 =====
  frontend:          # React + Nginx
    profiles: [dev, nas]
  gpu-service:       # GPU 转录
    profiles: [dev]

  # ===== 按需服务 =====
  bilibili-monitor:  # B站采集(非常驻)
    profiles: [dev, nas]
  bilibili-cron:     # 定时调度
    profiles: [nas]

volumes:
  duckdb-data:       # 结构化数据
  chromadb-data:     # 向量数据
  bilibili-data:     # 音频/转写/checkpoint
  bilibili-cron-logs: # 定时任务日志

8 个服务,4 个 Volume。每个服务通过 profiles 标签控制在哪个环境下启动。

三、Profiles 区分环境

1. 核心思路

Docker Compose 的 profiles 功能可以标记每个服务属于哪个"环境"。启动时指定 profile,只有匹配的服务才会启动。

yaml 复制代码
services:
  chromadb:
    profiles: [dev, nas]    # 两个环境都启动
  
  gpu-service:
    profiles: [dev]         # 仅开发环境(有 GPU)
  
  bilibili-cron:
    profiles: [nas]         # 仅生产环境(定时采集)

这样同一个 YAML 文件就能服务两个环境,不需要维护两份配置文件。

2. 两种环境对比

服务 dev(开发机) nas(NAS) 说明
chromadb 向量数据库,两边都要
text-to-sql SQL 查询服务
rag RAG 检索服务
router-agent 路由 Agent
frontend 前端
gpu-service 需要 NVIDIA GPU
bilibili-monitor 按需运行
bilibili-cron 定时采集仅生产环境

启动命令:

bash 复制代码
# 开发机(含 GPU 转录)
docker compose --profile dev up -d

# NAS 生产(含定时调度)
docker compose --profile nas up -d

四、服务拆解

接下来逐个拆解每个服务的配置。

1. 基础设施:ChromaDB

ChromaDB 是向量数据库,存的是文档的 Embedding 向量。它用的是官方镜像,不需要自己写 Dockerfile。

yaml 复制代码
chromadb:
  image: chromadb/chroma:latest
  container_name: chromadb
  profiles: [dev, nas]
  ports:
    - "8001:8000"
  volumes:
    - chromadb-data:/data
  environment:
    - IS_PERSISTENT=TRUE
    - ANONYMIZED_TELEMETRY=FALSE
  restart: unless-stopped
  mem_limit: 1g
  healthcheck:
    test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/localhost/8000'"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 15s

几个关键点:

  • image: chromadb/chroma:latest:直接用官方镜像,不用自己构建
  • IS_PERSISTENT=TRUE :开启持久化,数据写到 /data 目录
  • 端口映射 8001:8000:容器内是 8000,映射到宿主机的 8001(避免和其他服务冲突)
  • mem_limit: 1g:NAS 只有 8GB 内存,每个服务都得限制
  • healthcheck:ChromaDB 官方镜像没有内置健康检查,这里用 bash 的 TCP 连接检测替代 curl

2. Text-to-SQL 服务

yaml 复制代码
text-to-sql:
  build:
    context: ./text-to-sql
    dockerfile: Dockerfile
  container_name: text-to-sql
  profiles: [dev, nas]
  ports:
    - "8010:8010"
  environment:
    - MINIMAX_API_KEY=${MINIMAX_API_KEY}
    - MINIMAX_BASE_URL=${MINIMAX_BASE_URL:-https://api.minimaxi.com/anthropic}
    - DATABASE_PATH=/app/data/content.db
  volumes:
    - duckdb-data:/app/data
  restart: unless-stopped
  mem_limit: 2g
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:8010/"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 10s
  • build.context: ./text-to-sql:Dockerfile 在 text-to-sql 目录下
  • DATABASE_PATH=/app/data/content.db:DuckDB 数据库文件路径
  • duckdb-data:/app/data:DuckDB 数据存在 Named Volume 里,多个服务共享
  • mem_limit: 2g:Text-to-SQL 用了 MiniMax M2.7 做 SQL 生成,内存消耗较大

3. RAG 服务

yaml 复制代码
rag:
  build:
    context: .
    dockerfile: personal-knowledge-rag/Dockerfile
  container_name: rag
  profiles: [dev, nas]
  ports:
    - "8090:8090"
  environment:
    - LLM_PROVIDER=${LLM_PROVIDER:-deepseek}
    - LLM_MODEL=${RAG_LLM_MODEL:-deepseek-v4-pro}
    - REFINE_API_KEY=${REFINE_API_KEY}
    - REFINE_API_URL=${REFINE_API_URL}
    - REFINE_MODEL=${REFINE_MODEL:-deepseek-v4-flash}
    - SILICONFLOW_API_KEY=${SILICONFLOW_API_KEY}
    - CHROMA_HOST=chromadb
    - CHROMA_PORT=8000
  volumes:
    - ./personal-knowledge-rag/video_knowledge:/app/video_knowledge
    - ./personal-knowledge-rag/chroma_db:/app/chroma_db
  depends_on:
    chromadb:
      condition: service_healthy
  restart: unless-stopped
  mem_limit: 2g

RAG 服务的 build context 是 .(项目根目录),因为它的 Dockerfile 需要访问 shared/ 目录(共享的 Python 模块)。这是一个容易踩的坑------如果 context 设成子目录,COPY shared/ 就会找不到文件。

注意 depends_on 用了 condition: service_healthy,确保 ChromaDB 健康检查通过后才启动 RAG。

4. Router Agent

Router Agent 是整个系统的入口,需要透传很多环境变量:

yaml 复制代码
router-agent:
  build:
    context: ./router-agent
    dockerfile: Dockerfile
  container_name: router-agent
  profiles: [dev, nas]
  ports:
    - "8000:8000"
  environment:
    - CHAT_API_KEY=${MINIMAX_API_KEY}
    - SQL_SERVICE_URL=http://text-to-sql:8010
    - RAG_SERVICE_URL=http://rag:8090
    # 透传给 bilibili-monitor(通过 Docker SDK 触发)
    - BILIBILI_COOKIE=${BILIBILI_COOKIE}
    - REFINE_API_URL=${REFINE_API_URL}
    - REFINE_API_KEY=${REFINE_API_KEY}
    - REFINE_MODEL=${REFINE_MODEL:-deepseek-v4-flash}
  volumes:
    - duckdb-data:/app/data
    - /var/run/docker.sock:/var/run/docker.sock
    - ./bilibili-monitor/config:/app/bilibili-config
  depends_on:
    text-to-sql:
      condition: service_healthy
    rag:
      condition: service_healthy

这里有一个特别重要的配置:/var/run/docker.sock:/var/run/docker.sock

Router Agent 需要通过 Docker SDK 动态启动 bilibili-monitor 容器来执行采集任务。挂载 Docker socket 后,容器内就能控制宿主机的 Docker daemon。这是一个比较"暴力"的做法,但在单机场景下是最简单的方案。

SQL_SERVICE_URLRAG_SERVICE_URL 用的是 Docker 内部 DNS(容器名直接当主机名),不需要写 IP 地址。

5. 前端 + Nginx

yaml 复制代码
frontend:
  build:
    context: ./text-to-sql/frontend
    dockerfile: Dockerfile
  container_name: frontend
  profiles: [dev, nas]
  ports:
    - "80:80"
  volumes:
    - ./text-to-sql/frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
  depends_on:
    - router-agent
    - text-to-sql
  restart: unless-stopped
  mem_limit: 256m

前端用的是多阶段构建:先 npm run build 编译 React 代码,再把产物 COPY 到 Nginx 镜像里。:ro 表示只读挂载,防止容器内意外修改 nginx 配置。

内存限制只给了 256m,因为 Nginx 本身非常轻量。

6. GPU 转录服务(仅开发机)

yaml 复制代码
gpu-service:
  build:
    context: ./bilibili-monitor
    dockerfile: Dockerfile.gpu
  container_name: gpu-service
  profiles: [dev]
  ports:
    - "8011:8011"
  deploy:
    resources:
      reservations:
        devices:
          - driver: nvidia
            count: all
            capabilities: [gpu]
  mem_limit: 4g

deploy.resources.reservations.devices 这个配置是用来做 GPU 直通的。需要宿主机安装 NVIDIA Container Toolkit,Docker Compose 会自动把 GPU 设备映射到容器内。

注意 profiles: [dev],NAS 上没有 GPU,不会启动这个服务。

7. bilibili-monitor(按需运行)

bilibili-monitor 比较特殊------它不是常驻服务,只在采集时启动,跑完就退出。

yaml 复制代码
bilibili-monitor:
  build:
    context: .
    dockerfile: bilibili-monitor/Dockerfile
  container_name: bilibili-monitor
  profiles: [dev, nas]
  restart: "no"
  mem_limit: 4g
  command: ["echo", "bilibili-monitor 镜像就绪,等待前端触发采集..."]
  • restart: "no":不自动重启,跑完就退出
  • command 覆盖了 Dockerfile 的 CMD:默认只是打印一条消息,表示镜像就绪。实际采集是通过 Router Agent 的 Docker SDK 动态启动新容器来执行的

为什么不在这里直接跑采集?因为前端需要控制采集参数(选哪些 UP主、是否全量扫描),这些参数是通过 API 传给 Router Agent,再由 Router Agent 动态构建 docker run 命令来启动的。

8. bilibili-cron(定时调度)

yaml 复制代码
bilibili-cron:
  image: docker:cli
  container_name: bilibili-cron
  profiles: [nas]
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - ./bilibili-monitor/cron_entry.sh:/usr/local/bin/cron_entry.sh:ro
  entrypoint: /bin/sh
  command: >
    -c "
      apk add --no-cache bash > /dev/null 2>&1;
      chmod +x /usr/local/bin/cron_entry.sh;
      echo '17 */6 * * * /usr/local/bin/cron_entry.sh' > /etc/crontabs/root;
      echo '[bilibili-cron] 定时任务已启动(每6小时运行,:17 触发)';
      crond -f -l 2
    "
  restart: unless-stopped
  mem_limit: 128m

这个容器用的是 docker:cli 官方镜像(Alpine Linux + Docker CLI),里面跑一个 crond 守护进程。每 6 小时通过 cron_entry.sh 脚本触发一次 bilibili-monitor 容器。

为什么用 17 */6 而不是 0 */6?因为整点(:00)是各种定时任务的高峰期,错峰到 :17 可以减少同时启动的资源争抢。

只给了 128m 内存,因为 crond + docker cli 几乎不占资源。

五、Dockerfile 编写:国内镜像加速

在国内构建 Docker 镜像,最大的痛点就是下载慢。我在每个 Dockerfile 里都做了三级镜像加速。

1. apt 换阿里云镜像

dockerfile 复制代码
# 使用国内 Debian 镜像加速 apt 安装
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null; \
    sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null; \
    true

2>/dev/nulltrue 是为了兼容不同版本的 Debian 基础镜像。新版 Debian 的源配置在 /etc/apt/sources.list.d/debian.sources,旧版在 /etc/apt/sources.list,两个都改一遍,哪个报错忽略就行。

2. pip 换清华 TUNA 镜像

dockerfile 复制代码
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
    --trusted-host pypi.tuna.tsinghua.edu.cn \
    -r requirements.txt

--no-cache-dir 不缓存下载的安装包,减小镜像体积。--trusted-host 避免 HTTPS 证书问题。

3. npm 换淘宝镜像

前端的 Dockerfile 里,npm 安装也要换源:

dockerfile 复制代码
RUN npm config set registry https://registry.npmmirror.com
RUN npm ci

npm cinpm install 更适合 Docker 构建,它严格按照 package-lock.json 安装,更快也更稳定。

4. 完整的 bilibili-monitor Dockerfile

dockerfile 复制代码
FROM python:3.11-slim

# apt 换阿里云
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null; \
    sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null; \
    true

# 安装系统依赖(ffmpeg 用于音频转写)
RUN apt-get update && apt-get install -y --fix-missing \
    ffmpeg \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 复制共享模块
COPY shared/ ./shared/
ENV PYTHONPATH=/app/shared

# pip 换清华 TUNA
COPY bilibili-monitor/requirements.txt .
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
    --trusted-host pypi.tuna.tsinghua.edu.cn \
    -r requirements.txt

COPY bilibili-monitor/src/ ./src/
COPY bilibili-monitor/config/ ./config/

RUN mkdir -p downloads transcripts data chromadb

ENV PYTHONUNBUFFERED=1
ENV WHISPER_DEVICE=cpu
ENV WHISPER_MODEL=small
ENV COMPUTE_TYPE=int8

CMD ["python", "src/monitor_all.py"]

几个小技巧:

  • COPY shared/ ./shared/:bilibili-monitor 的构建上下文是项目根目录,所以能直接 COPY 根目录下的 shared/ 模块
  • ENV PYTHONPATH=/app/shared:让 Python 能找到 shared 模块
  • PYTHONUNBUFFERED=1:确保 Python 的 print 输出实时刷新到 Docker 日志,不被缓冲

截图:Docker build 过程,可以看到使用清华 pip 镜像的下载速度

六、Volume 与数据共享

1. 四个 Named Volume

yaml 复制代码
volumes:
  duckdb-data:         # DuckDB 数据库
  chromadb-data:       # ChromaDB 向量数据
  bilibili-data:       # B站原始数据(音频、转写、checkpoint)
  bilibili-cron-logs:  # 定时任务日志

数据共享关系:

复制代码
duckdb-data:/app/data
├── bilibili-monitor 写入(采集时写 video_meta)
├── text-to-sql 读取(SQL 查询)
└── router-agent 读取(查询日志 + 统计数据)

chromadb-data:/data
├── chromadb 管理(向量存储和检索)
└── bilibili-monitor 通过 HTTP API 写入

bilibili-data:/app/downloads
├── bilibili-monitor 写入(下载的音频、转写文本、checkpoint 文件)
└── gpu-service 读取(GPU 转写时访问音频文件)

duckdb-data 是最关键的------bilibili-monitor 采集时把数据写进去,text-to-sql 和 router-agent 从里面读。三个容器共享同一个 Volume,数据天然一致。

2. Bind Mount

除了 Named Volume,还有一些用 Bind Mount(直接映射宿主机目录):

yaml 复制代码
# RAG 服务的 Bind Mount
volumes:
  - ./personal-knowledge-rag/video_knowledge:/app/video_knowledge
  - ./personal-knowledge-rag/chroma_db:/app/chroma_db

# Router Agent 的 Bind Mount
volumes:
  - ./bilibili-monitor/config:/app/bilibili-config

Bind Mount 的好处是宿主机可以直接看到和编辑文件,方便调试。缺点是路径和宿主机绑定,换台机器可能需要改路径。

3. Docker Socket 挂载

yaml 复制代码
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Router Agent 和 bilibili-cron 都挂载了 Docker socket,这样它们可以在容器内控制宿主机的 Docker daemon(启动/停止其他容器)。

这是单机部署的常见做法,但需要注意安全风险------如果容器被攻破,攻击者就能控制整个 Docker daemon。在生产环境中,更安全的方案是使用 Docker API 的 TLS 认证。

七、环境变量管理

1. .env 文件结构

所有的 API Key 和可配置项都放在项目根目录的 .env 文件里(不提交到 Git):

bash 复制代码
# Embedding API(SiliconFlow)
EMBEDDING_PROVIDER=siliconflow
EMBEDDING_API_KEY=sk-xxx
EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1
EMBEDDING_MODEL=BAAI/bge-large-zh-v1.5

# Chat API(MiniMax M2.7)
CHAT_API_KEY=sk-xxx
CHAT_BASE_URL=https://api.minimaxi.com/anthropic
CHAT_MODEL=MiniMax-M2.7

# DeepSeek(精炼 + RAG 问答)
REFINE_API_URL=https://api.deepseek.com/v1/chat/completions
REFINE_API_KEY=sk-xxx
REFINE_MODEL=deepseek-v4-flash

# B站 Cookie
BILIBILI_COOKIE=

# Whisper 转写
WHISPER_DEVICE=cpu
WHISPER_MODEL=small
COMPUTE_TYPE=int8

.env 文件会被 Docker Compose 自动读取,通过 ${变量名} 语法注入到容器里。

2. 变量透传

Router Agent 是一个"中间人"------它自己需要 MiniMax API Key 来做意图分类,同时还需要把 bilibili-monitor 的各种环境变量透传下去(因为采集是通过 Docker SDK 动态启动的):

yaml 复制代码
router-agent:
  environment:
    # 自己用的
    - CHAT_API_KEY=${MINIMAX_API_KEY}
    - SQL_SERVICE_URL=http://text-to-sql:8010
    - RAG_SERVICE_URL=http://rag:8090
    
    # 透传给 bilibili-monitor
    - BILIBILI_COOKIE=${BILIBILI_COOKIE}
    - REFINE_API_URL=${REFINE_API_URL}
    - REFINE_API_KEY=${REFINE_API_KEY}
    - REFINE_MODEL=${REFINE_MODEL:-deepseek-v4-flash}
    - WHISPER_DEVICE=${WHISPER_DEVICE:-cpu}
    - WHISPER_MODEL=${WHISPER_MODEL:-small}

3. 默认值语法

Docker Compose 支持 ${变量:-默认值} 语法:

yaml 复制代码
- REFINE_MODEL=${REFINE_MODEL:-deepseek-v4-flash}

意思是:如果 .env 里配了 REFINE_MODEL,就用配的值;没配就用 deepseek-v4-flash。这样即使用户的 .env 里漏了某个变量,服务也能正常启动。

八、健康检查与依赖关系

1. healthcheck 配置

每个常驻服务都配了健康检查,确保服务真的可用后才算"启动完成":

yaml 复制代码
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8010/"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s
  • test :用 curl 请求服务的健康检查端点,-f 表示 HTTP 4xx/5xx 时返回非零退出码
  • interval: 30s:每 30 秒检查一次
  • start_period: 10s:启动后前 10 秒不算失败(给服务初始化的时间)
  • retries: 3:连续失败 3 次才标记为 unhealthy

ChromaDB 的健康检查比较特殊,因为官方镜像里没有 curl:

yaml 复制代码
healthcheck:
  test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/localhost/8000'"]

用 bash 内置的 /dev/tcp 做 TCP 连接检测,不需要安装额外的工具。

2. depends_on 启动顺序

yaml 复制代码
rag:
  depends_on:
    chromadb:
      condition: service_healthy    # 等 ChromaDB 健康

router-agent:
  depends_on:
    text-to-sql:
      condition: service_healthy    # 等 Text-to-SQL 健康
    rag:
      condition: service_healthy    # 等 RAG 健康

frontend:
  depends_on:
    - router-agent                  # 等 Router Agent 启动
    - text-to-sql                   # 等 Text-to-SQL 启动

最终的启动顺序:

复制代码
chromadb(healthy)
  → rag(healthy)
  → text-to-sql(healthy)
    → router-agent(healthy)
      → frontend

condition: service_healthy 比简单的 depends_on 更可靠------它不仅等服务启动,还等健康检查通过。这避免了 Router Agent 在 RAG 还没完全就绪时就开始转发请求的问题。

九、Nginx 反向代理配置

前端容器同时承担 Nginx 反向代理的职责,完整配置:

nginx 复制代码
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Docker 内置 DNS,避免容器重建后 IP 变化导致 502
    resolver 127.0.0.11 valid=10s;

    # SPA 路由 - 所有非文件路径都返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 代理 Router Agent API
    location /api/ {
        client_max_body_size 500m;
        set $upstream_router http://router-agent:8000;
        proxy_pass $upstream_router;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 120s;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

几个细节:

  1. resolver 127.0.0.11 :Docker 的内置 DNS 服务器。用 set $upstream_router 变量而不是直接写 proxy_pass http://router-agent:8000,是因为 Nginx 只在启动时解析 DNS。如果 router-agent 容器重建后 IP 变了,直接写死会导致 502。用变量 + resolver 可以让 Nginx 定期重新解析。

  2. client_max_body_size 500m:默认 Nginx 只允许 1MB 的上传。我们需要支持 UP主导入(ZIP 文件可能几百 MB),所以调到 500m。这是一个实际的 bug 修复------之前用户导入大文件时直接 413 错误。

  3. try_files $uri $uri/ /index.html:React SPA 的路由处理。所有非文件路径都返回 index.html,让前端路由接管。

  4. 静态资源缓存 1 年:Vite 构建的静态文件带 hash,可以放心长缓存。

截图:Nginx 反向代理架构图------浏览器 :80 → Nginx → /api/ 转发到 router-agent:8000

十、一键启动与验证

全部配置好之后,启动整个系统只需要两步:

bash 复制代码
# 1. 配置环境变量
cp .env.example .env
# 编辑 .env,填入 API Key

# 2. 一键启动
docker compose --profile dev up -d --build

--build 参数会在启动前重新构建镜像。如果代码没有变化,Docker 会使用缓存,不会重新下载依赖。

验证所有服务是否正常:

bash 复制代码
docker compose ps

预期输出:

复制代码
NAME          STATUS
chromadb      Up (healthy)
text-to-sql   Up (healthy)
rag           Up (healthy)
router-agent  Up (healthy)
frontend      Up

测试问答功能:

bash 复制代码
curl -X POST http://localhost:8000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"question": "知识库有多少个视频"}'

或者直接打开浏览器访问 http://localhost,就能看到前端界面了。

总结

Docker Compose 把 7 个容器的端口映射、环境变量、Volume 挂载、依赖关系、健康检查全部编排在一个 YAML 文件里,一行命令启动整个系统。profiles 机制让同一个文件服务两套环境,国内镜像加速让构建速度从"去喝杯咖啡"变成"去倒杯水"。下一篇讲 bilibili-monitor 的视频自动采集。

相关推荐
winlife_4 小时前
全程用 AI 做一款商业级手游 · EP1 地基:先搭框架层,不急着写玩法
unity·ai编程·游戏架构·mcp·框架设计·funplay
春风野草5 小时前
第五章 记忆系统不是假装记住——3层记忆架构的坑与遗忘的艺术
人工智能·ai编程
咖啡星人k6 小时前
MonkeyCode 多模型路由机制:AI编程工具如何智能选择最优模型
ai编程
小橙讲编程6 小时前
一键给 AI Agent 装上「互联网眼睛」:Agent Reach 深度解析与实战指南
人工智能·开源·github·ai编程
runnerdancer7 小时前
从能用到可靠:AI Agent 规范治理的工程实践
ai编程
youcans_7 小时前
【跟我学 AI 编程】(6) Claude Code 与 IDE 的集成
ide·人工智能·ai编程·claude code
码途漫谈8 小时前
Compound Engineering:让每一次开发都给下一次铺路
开源·ai编程
TFHoney9 小时前
当 AI 真正走进你的终端:Claude Code 使用指南
java·人工智能·ai编程
ZFSS9 小时前
VS Code + Serp MCP:让 Copilot 实时上网查询
人工智能·ai·ai作画·copilot·ai编程·ai写作