「从零到 AI 应用工程师」专栏 · 第 7 篇
到上一篇,你的对话服务已经有了:接口、分层、统一错误、PostgreSQL 历史、Redis 缓存。
但「在我电脑上能跑」和「别人(或未来的你)十分钟能跑」之间,还差一口气。本机装数据库、改端口、环境不一致......这些摩擦会吃掉大量热情。
今天把它们收成一句:
bash
docker compose up -d --build
应用、Postgres、Redis 一起起来;再用 /healthz 证明依赖真的可用------不只是容器显示 Up。
一、先把三个词分清
| 概念 | 一句话 |
|---|---|
| 镜像 Image | 只读的构建产物,像安装包 |
| 容器 Container | 镜像跑起来的进程实例 |
| Compose | 用一份 YAML 声明「多容器怎么一起跑」 |
Compose 里,服务名就是容器网络里的主机名 。所以应用连数据库应写 db,连 Redis 应写 redis------不要写 127.0.0.1。
127.0.0.1 在容器里指的是这个容器自己,不是你的笔记本电脑,也不是旁边的数据库容器。这是新手踩得最多的坑。
二、最小 Dockerfile
dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# 先装依赖,再拷代码 ------ 利用层缓存,改代码不必每次重装包
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
两个细节:
- 监听
0.0.0.0,否则容器外访问不到; requirements.txt与源码分开 COPY,改一行业务代码不会让依赖层全部失效。
三、docker-compose.yml(应用 + 库 + 缓存)
yaml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: chat_db
volumes:
- pg_data:/var/lib/postgresql/data
- ./sql/init.sql:/docker-entrypoint-initdb.d/01_init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d chat_db"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
web:
build: .
env_file: .env.docker
ports:
- "127.0.0.1:8000:8000" # 只绑本机,降低误暴露风险
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test:
[
"CMD-SHELL",
"python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')\"",
]
interval: 10s
timeout: 5s
retries: 10
volumes:
pg_data:
redis_data:
.env.docker 示例(不要提交真实密钥):
env
DB_HOST=db
DB_PORT=5432
DB_USER=app_user
DB_PASSWORD=请换成强密码
DB_NAME=chat_db
REDIS_HOST=redis
REDIS_PORT=6379
API_TOKEN=请换成你的令牌
MODEL_API_KEY=sk-你的密钥
MODEL_API_URL=https://api.example.com/v1/chat/completions
MODEL_NAME=your-model-name
应用代码里读库、读缓存,主机名要用环境变量里的 db / redis。
四、健康检查:Up 不等于可用
depends_on: condition: service_healthy 比「只等容器启动」强一截,但应用自己仍应提供:
text
GET /ping → 进程活着
GET /healthz → 进程 + 数据库 + Redis(必要时再加模型配置检查)
示例逻辑:
python
@router.get("/healthz")
async def healthz():
db_ok = check_db() # SELECT 1
redis_ok = check_redis() # PING
ready = db_ok and redis_ok
status_code = 200 if ready else 503
return JSONResponse(
status_code=status_code,
content={
"code": status_code,
"message": "ok" if ready else "dependency not ready",
"data": {"db": db_ok, "redis": redis_ok},
},
)
验收时:容器 Up 只是起点,/healthz 返回 200 才算栈就绪。
五、常用命令
bash
# 构建并后台启动
docker compose up -d --build
# 看状态
docker compose ps
# 看日志(出问题先看这里)
docker compose logs --tail=100 web
docker compose logs --tail=100 db
# 停掉(默认保留数据卷)
docker compose down
# 停掉并删除数据卷 ------ 危险,本地数据会没
# docker compose down -v
改了 .env 或依赖:通常需要 重建容器 ,不是简单 restart:
bash
docker compose up -d --build --force-recreate web
六、验收清单
bash
docker compose up -d --build
docker compose ps
curl -i http://127.0.0.1:8000/ping
curl -i http://127.0.0.1:8000/healthz
curl -X POST http://127.0.0.1:8000/chat \
-H "Authorization: Bearer 你的令牌" \
-H "Content-Type: application/json" \
-d '{
"user_id": "docker_demo",
"session_id": "session_docker_01",
"message": "栈是否健康"
}'
全部通过,阶段 1 的「能交付最小后端」就齐了。
七、常见坑(建议贴显示器旁)
- 容器里把数据库地址写成
localhost。 → 改成服务名db。 - 密码、Token、模型 Key 写进镜像或打进 Git。 → 环境变量 /
env_file,示例用.env.example。 - 以为
depends_on等于数据库已可查询。 → 加 healthcheck;应用再做/healthz。 - 误用
docker compose down -v。 → 本地库被清空。 - 改了
init.sql却不见生效。 → 初始化脚本只在数据卷首次创建时跑;已有卷需要迁移或删卷重建(删卷会丢数据)。 - 生产把 5432 / 6379 映射到公网。 → 数据库和缓存只走内部网络;本示例仅把
8000绑在127.0.0.1。 - 改了依赖不
--build。 → 容器里仍是旧包。
八、阶段 1 小结:你现在手里有什么
从第 2 篇到第 7 篇,一条对话后端的骨架已经立住:
| 篇 | 能力 |
|---|---|
| 02 | FastAPI + 大模型,打通 /chat |
| 03 | 路由 / 业务 / 客户端分层 |
| 04 | 统一响应、统一异常、request_id |
| 05 | PostgreSQL 历史 + 分页 |
| 06 | Redis 缓存重复问题(可降级) |
| 07 | Docker Compose 一键拉起整栈 |
这不是玩具脚本了------这是后面做 RAG、做 Agent 时,可以继续往上长的底座。
九、带走这三条
- Compose 把应用、依赖、网络、卷、健康检查变成可重复执行的声明。
- 容器网络用服务名;持久数据用 Volume;密钥用环境注入。
- 验收看
/healthz和核心接口,不只看容器 Up。
下一阶段进入专栏最硬核的一块:RAG------让大模型读懂你自己的资料,还不许瞎编。 第 8 篇先把概念和全貌讲清,再进入解析、分块、向量检索。
这是专栏第 7 篇,也是 chat-api 阶段的收官。两天一更,RAG 见。