容器化与上线

第 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=xxxARG 注入真密钥,用 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。
笔记加载中…