容器化与上线
第 28 章的 FastAPI 服务在开发机跑通了,但"换个机器还能不能跑起来"是另一回事。容器化把依赖、代码、启动命令打包成镜像,配合 docker compose 管理密钥、健康检查与日志,几分钟就能把智能体服务部署到任意服务器。本章沿用第 28 章的 app.py(含 /healthz),给出完整可复制的一组文件与部署步骤。
项目结构
agent-svc/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── .env.example # 模板入库,真实 .env 不入库
├── app.py # 第 28 章
├── agent.py # 第 28 章
└── docker-compose.yml
Dockerfile
# 用 slim 底镜像,体积小、攻击面小
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai # 时区:容器默认 UTC,日志时间会差 8 小时
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # 先拷依赖再拷代码,利用镜像层缓存
COPY . .
EXPOSE 8000
# 日志直接打 stdout/stderr,交给 docker logs 收集
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt 与 .dockerignore
# requirements.txt
fastapi
uvicorn
langgraph
langchain-openai
langchain-core
pydantic
# .dockerignore:避免把密钥/缓存/虚拟环境打进镜像
.env
*.pyc
__pycache__/
.venv/
venv/
tests/
.git/
*.db
镜像里绝不能出现真实密钥:.dockerignore 排除 .env 后,即使忘了删也进不了构建上下文。
docker-compose.yml:服务 + 可选 Redis
services:
api:
build: .
restart: unless-stopped
env_file: .env # 密钥全部由宿主机 .env 注入,不写死在文件里
environment:
- TZ=Asia/Shanghai
ports:
- "8000:8000"
healthcheck: # slim 镜像没有 curl,用 python 探测 /healthz
test: ["CMD", "python", "-c",
"import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
logging:
driver: json-file # 默认即 json-file;应用日志走 stdout,docker logs 直接看
redis:
image: redis:7-alpine
restart: unless-stopped
# 可选:只有会话/任务队列需要 Redis 时才用(第 20 章的持久化 checkpointer);
# 不需要时整段删除即可,api 不依赖它也能启动。
# .env.example(复制为 .env 后填写真实值)
LLM_API_KEY=sk-xxxx
LLM_MODEL=deepseek-chat
LLM_BASE_URL=https://api.deepseek.com
TZ=Asia/Shanghai
部署步骤
# 1) 服务器上准备 .env(真实密钥只放这里)
cp .env.example .env
# 2) 构建并启动(首次构建稍慢,之后走层缓存)
docker compose up -d --build
# 3) 确认健康
docker compose ps # 输出:agent-svc-api-1 Up (healthy)
curl http://127.0.0.1:8000/healthz # 输出:{"status":"ok"}
# 4) 看日志(应用 print/uvicorn 日志都在这里)
docker compose logs -f api
# 5) 代码更新后重新构建部署
docker compose up -d --build --no-deps api
Nginx 反代一句
80 端口由 Nginx 托管,/api 反代到服务容器(同一 compose 网络内可用服务名 api):
location /api/ {
proxy_pass http://api:8000/; # 同一 compose 项目,服务名即主机名
proxy_set_header Host $host;
proxy_buffering off; # 流式 SSE 必须关缓冲,否则前端收不到逐字
proxy_read_timeout 300s; # Agent 长任务放宽超时
}
SSE 接口特别要求 proxy_buffering off,否则 Nginx 会把数据攒满才转发,打字机效果失效。
常见坑
- 时区:容器默认 UTC,日志时间差 8 小时——Dockerfile 与 compose 都设 TZ=Asia/Shanghai,代码里 datetime.now() 再配合 zoneinfo 更稳;
- 密钥泄漏:密钥只放 .env(不进镜像、不进仓库);镜像构建历史里别用
ENV LLM_API_KEY=xxx或ARG注入真密钥,用 docker history 能查出来; - 忘了 .dockerignore:会把 .env、.venv、测试文件打进镜像,镜像又大又泄密;
- slim 无 curl:健康检查别用 curl 命令,用上面的 python 探测,或在基础镜像阶段安装 curl;
- 健康检查不当:healthcheck 没配 start_period 时,服务冷启动慢会被反复重启,给足 start_period;
- 升级只重建一个服务:改代码执行
up -d --build --no-deps api,避免把 Redis 也一起重建。 小结:容器化上线 = Dockerfile(slim 底镜像 + 先装依赖后拷代码 + stdout 日志)+ .dockerignore 挡密钥 + compose 用 env_file 注入密钥、python 探测健康、stdout 收集日志;需要长会话时加 Redis 与 checkpointer,前端流式经 Nginx 记得关掉 proxy_buffering。