Appearance
第十四阶段: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-client3. 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 异步任务(耗时的工作放到后台处理)。