Skip to content

第十四阶段:Dockerfile 与 Docker Compose ​

上一阶段 Qdrant 已经跑在 Docker 里,但 API 还是在本机直接运行。这一阶段把 API 也装进容器,用一条命令同时启动两个服务。

一、两个核心概念 ​

  • Dockerfile 是"装机清单":从哪个基础系统开始、装哪些库、复制哪些文件、启动时运行什么命令。Docker 照着它把程序做成镜像。
  • docker compose 描述多个容器以及它们之间的连接:有哪些服务、各自映射哪些端口、用什么环境变量、谁依赖谁。一条命令就能把整套服务启动起来。

二、容器里的 localhost 是谁 ​

最容易踩的坑:容器里的 localhost 指的是容器自己,不是你的电脑。

  • 连 Qdrant:用 compose 里的服务名 qdrant:6333。compose 会让同一组里的容器可以用服务名互相访问。
  • 连本机的 LM Studio:用 host.docker.internal:1234。这是 Docker 提供的特殊地址,指向宿主机,也就是你的电脑。

所以代码不能把地址写死,改成从两个环境变量读取:QDRANT_URL 和 EMBED_URL。不设置时,默认值还是 localhost,在本机直接运行也照样能用。

三、文件放在哪 ​

所有文件都放在项目根目录(和 .env 同一层)。Docker 会在当前目录里按固定文件名查找 Dockerfile 和 docker-compose.yml,名字错一个字母都找不到。

记事本的坑

用记事本保存时,Dockerfile 可能会变成 Dockerfile.txt。在资源管理器里打开"文件扩展名"显示检查一下,或者直接在 VS Code 里新建文件。

四、代码 ​

1. api.py ​

和 13_api.py 基本一样,只是把两个地址改成了从环境变量读取。文件名改成字母开头,是为了让 uvicorn api:app 可以直接导入。

提示

model_name 里的"这里填你的 Embedding 模型名"是占位文字。LM Studio 只加载了一个 Embedding 模型时,照这样写也能运行;想写规范一点,可以换成 LM Studio 里显示的模型名。

python
import os
import qdrant_client
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.qdrant import QdrantVectorStore
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

load_dotenv()

# 启动时只加载一次索引(复用项目 1 的 kb_storage),不要每次请求都加载
Settings.embed_model = OpenAILikeEmbedding(
    model_name="这里填你的 Embedding 模型名",
    api_base=os.getenv("EMBED_URL", "http://localhost:1234/v1"), api_key="<redacted>")

client = qdrant_client.QdrantClient(url=os.getenv("QDRANT_URL", "http://localhost:6333"))

index = VectorStoreIndex.from_vector_store(QdrantVectorStore(client=client, collection_name="kb"))
retriever = index.as_retriever(similarity_top_k=3)

llm = OpenAI(api_key=<redacted>("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"))

app = FastAPI(title="知识库问答服务")

# 请求体格式:{"question": "..."},FastAPI 会自动校验
class AskRequest(BaseModel):
    question: str

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/ask")
def ask(req: AskRequest):
    nodes = retriever.retrieve(req.question)
    context = "\n\n".join(n.get_content() for n in nodes)
    resp = llm.chat.completions.create(
        model=os.getenv("LLM_MODEL"),
        messages=[
            {"role": "system", "content": "根据资料回答,资料里没有就说不知道。\n资料:\n" + context},
            {"role": "user", "content": req.question},
        ])
    return {"answer": resp.choices[0].message.content,
            "sources": [round(n.score, 3) for n in nodes]}

2. requirements.txt ​

容器里是一个全新的 Python 环境,需要装哪些库都列在这里:

text
fastapi
uvicorn
python-dotenv
openai
llama-index-core
llama-index-embeddings-openai-like
llama-index-vector-stores-qdrant
qdrant-client

3. Dockerfile ​

dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY api.py .
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
  • -i https://pypi.tuna.tsinghua.edu.cn/simple:用清华 pip 镜像下载,国内速度快很多。
  • 先复制 requirements.txt 并安装,再复制 api.py。只改代码时,安装依赖那一步可以直接用缓存,不用重新装。
  • --host 0.0.0.0:让 uvicorn 接收所有来源的请求。默认只监听容器内部的 127.0.0.1,外面的请求进不来,端口映射也就没用了。

4. docker-compose.yml ​

yaml
services:
  qdrant:
    image: qdrant/qdrant
    ports: ["6333:6333"]
    volumes: [qdrant_data:/qdrant/storage]
  api:
    build: .
    ports: ["8000:8000"]
    env_file: .env          # LLM_API_KEY 等运行时注入,不会打包进镜像
    environment:
      QDRANT_URL: http://qdrant:6333
      EMBED_URL: http://host.docker.internal:1234/v1
    depends_on: [qdrant]
volumes:
  qdrant_data:
    external: true          # 复用上一阶段已经建好索引的数据卷
  • env_file: .env:在运行时注入密钥。API Key 不会被打包进镜像,就算镜像分享给别人也不会泄露。
  • environment:设置 QDRANT_URL 和 EMBED_URL,地址按第二节的规则写。
  • depends_on:先启动 qdrant,再启动 api。
  • external: true:使用上一阶段 docker run 时已经创建的 qdrant_data 数据卷,直接复用里面建好的索引,不用重新建。

5. .dockerignore ​

text
.env
.venv
data
kb_storage
chroma_db
output
__pycache__

构建镜像时,这些文件和文件夹不会被发送给 Docker:既防止 .env 里的密钥被打包进镜像,也避免把 .venv、data 这类大文件夹传进去拖慢构建。

五、启动 ​

bash
docker rm -f qdrant
docker compose up -d --build
  • 第一条:删掉上一阶段用 docker run 启动的 qdrant 容器,否则 6333 端口会冲突。数据存在数据卷里,删容器不会丢数据。
  • 第二条:--build 根据 Dockerfile 构建镜像,-d 表示后台运行。实测首次构建大约 138 秒,主要时间花在安装依赖上,以后会快很多。

踩坑:docker 无法识别

VS Code 的终端是在安装 Docker 之前打开的,没有读到新的 PATH 环境变量,所以提示 docker 无法识别。重启 VS Code 就好。

六、排障 ​

bash
docker compose ps          # 查看各个容器是否在运行
docker compose logs api    # 查看 api 容器的日志,报错信息都在这里

如果日志显示连不上 Embedding 服务,可能需要在 LM Studio 的服务设置里打开局域网访问(允许本机以外的地址访问)。对 LM Studio 来说,从容器发来的请求不算来自本机。

七、实测 ​

打开 http://localhost:8000/docs,提问"可以保修吗":

  • 返回 200;
  • 回答很稳妥,按资料说明了保修条件,没有乱下结论;
  • sources 是 [0.614, 0.501, 0.403]。

现在 API 和 Qdrant 都跑在容器里了,换一台装了 Docker 的电脑,也是同样一条命令就能启动。

下一阶段预告 ​

第十五阶段:加上 Redis 缓存(同样的问题不再重复调用 LLM)和 Celery 异步任务(耗时的工作放到后台处理)。