本篇对应
Dockerfile、docker-compose.yml、.github/workflows/ci.yml与 README「安全说明」。开发完成只是半场,把应用安全地交给运维才是终点。
一、Dockerfile 逐行讲
FROM python:3.12-slim
slim 变体:Python 官方镜像去掉编译工具链,体积从 1GB 降到 ~150MB。本仓库无 C 扩展编译需求(numpy 用官方 wheel),slim 足够。基础镜像要锁版本(3.12,与 requirements.txt 注释"与开发环境实测一致(Python 3.12)"对齐)。
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
两个标准 ENV:不写 .pyc(容器里没用),stdout/stderr 不缓冲(日志实时可见——容器排障全靠日志)。
# 先装依赖再拷代码:依赖层不变时利用 Docker 缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY static ./static
层的顺序就是缓存策略:requirements.txt 很少变、代码天天变。先 COPY 依赖清单装依赖,再 COPY 代码——改代码只重跑后两步,依赖层走缓存(装一次几十秒 → 变成秒级重建)。先拷代码再装依赖是 Dockerfile 最常见的反模式。--no-cache-dir 不留 pip 下载缓存,镜像更瘦。
# 非 root 运行:容器被攻破时降低影响面
RUN useradd --create-home --uid 10001 appuser \
&& mkdir -p /app/data \
&& chown -R appuser:appuser /app
USER appuser
容器内进程默认 root——一旦应用被攻破(比如 08 篇的沙箱被逃逸),攻击者就是容器内 root。切到 uid 10001 的普通用户,影响面骤减。"降影响面"思维贯穿本仓库(Docker 非 root、CSP 限外传、上传限量、沙箱白名单)。
VOLUME ["/app/data"]
EXPOSE 8005
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD python -c "import urllib.request,os; urllib.request.urlopen('http://127.0.0.1:8005/api/health', timeout=3)" || exit 1
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8005"]
VOLUME:SQLite 数据落/app/data卷,容器重建数据不丢;HEALTHCHECK:每 30 秒请求一次/api/health(03 篇那个接口终于派上正经用场),连续失败容器标记 unhealthy——编排系统据此重启;CMD用 exec 形式(JSON 数组):进程直接是 python 而不是 shell 的子进程,信号(SIGTERM)直达 uvicorn,优雅关闭才能生效。
二、docker-compose.yml:生产化参数
services:
sinan-ai-platform:
build: .
ports: ["8005:8005"]
volumes: ["./data:/app/data"]
environment:
# 完整变量清单见仓库 docker-compose.yml(${VAR:-} 语法 = 从宿主读、空则留空):
# · 模型接入三件套(Base URL / API Key / 模型名,留空则不启用)
# · AIP_CHAT_RATE_PER_MIN=30 / AIP_WF_RATE_PER_MIN=20(限流)
# · AIP_HTTP_API_ALLOW_PRIVATE=1(http_api 私网开关)
# · AIP_SECRET_KEY / AIP_ADMIN_PASSWORD(密钥与初始口令,见下文清单)
# 容器加固:资源上限 + 禁止提权
mem_limit: 512m
cpus: 1.0
pids_limit: 128
security_opt:
- no-new-privileges:true
restart: unless-stopped
逐条解释"为什么":
- 环境变量透传:
${VAR:-}语法 = 从宿主.env/环境读,空则留空。默认值注释写明关键一条:"生产对接真实外网系统时务必设为 0(禁止工作流 http_api 节点访问私网/回环地址)"——08 篇 SSRF 防护的部署侧开关。AIP_SECRET_KEY设置后 API Key 加密密钥不再自动生成文件(06 篇 secretbox);AIP_ADMIN_PASSWORD设置后首次初始化的 admin 不再强制改密(04 篇种子逻辑); - 资源上限三件套:内存 512m / CPU 1 核 / 进程数 128——失控的容器(死循环、内存泄漏、fork 炸弹)只烧掉配额,不拖垮宿主机。单机内部工具,这个配额绰绰有余;
no-new-privileges:true:容器内进程无法通过 setuid 等机制提权,与 Dockerfile 的非 root 形成双保险;restart: unless-stopped:崩溃自动拉起,人工 stop 的不算。
启动与验证:
docker compose up --build -d
docker compose logs -f # 看到 Uvicorn running 即成功
curl http://127.0.0.1:8005/api/health
docker compose ps # STATUS 显示 Up (healthy) 说明 HEALTHCHECK 通过
教程实测说明:本教程的核对环境(Windows 10 + Python 3.13)未安装 Docker,以上命令取自仓库 docker-compose.yml 与 README 的声明用法,未经本机实跑;其余篇目的 Python/pytest/uvicorn 命令均为实测。
三、GitHub Actions CI
name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: 安装依赖
run: pip install -r requirements.txt
- name: 单元与接口测试
run: python -m pytest tests -q
- name: RAG 检索效果回归
run: python scripts/eval_rag.py
要点:
- push 与 PR 都跑:PR 阶段拦住问题,push 后再确认一次;
cache: pip:依赖缓存让 CI 从"每次装依赖"变成秒级恢复;- 两个步骤就是两道质量门:107 项测试(功能对不对)+ 评估脚本(效果好不好,07/11 篇的"评估进 CI")。README 的绿色 CI 徽章背后就是这两行。
四、上线安全清单(逐条来自仓库的明确要求)
对外部署前,逐条打勾:
- [ ] 改掉默认口令:README"安全说明"原话——"默认口令
admin / admin123仅供本地演示,任何对外部署前请先修改"。最佳路径:设AIP_ADMIN_PASSWORD环境变量再初始化,或首次登录按提示强制改密; - [ ] 配置 HTTPS:生产环境在反代层(Nginx/Caddy)终结 TLS。Bearer token 明文跑 HTTP = 局域网内裸奔;
- [ ] 收紧 http_api 私网访问:
AIP_HTTP_API_ALLOW_PRIVATE=0(除非确实要调内网 API); - [ ] 固定加密密钥:设
AIP_SECRET_KEY,否则自动生成的data/.secret_key丢了/换了,settings 里的 API Key 全部解不开; - [ ] 数据备份:SQLite = 备份
data/目录(含platform.db与-wal文件)。建议定时任务冷备; - [ ] 知晓扩容边界:README 原话——"平台定位为单机内部工具:横向扩容需将限流计数外置(Redis)并迁移数据库(见 Roadmap)"。单机部署 = 限流、登录锁、内部密钥都存进程内存,多副本会失效;
- [ ] 最小暴露面:后台端口不必公网的就绑内网/加防火墙;
/api/admin/*本就有权限门,但不暴露更稳。
五、从开发到部署的完整命令流(全系列收束)
# 开发
git clone https://github.com/lianghui32/sinan-ai-platform.git
cd sinan-ai-platform
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
python -m pytest tests -q # 107 passed
python scripts/eval_rag.py # 生成评估报告
# 本地运行
python -m uvicorn app.main:app --host 0.0.0.0 --port 8005
# 部署
docker compose up --build -d
docker compose logs -f
六、结语:回到"每一步为什么"
13 篇走完,回看全系列的暗线:
- 01 篇的每条需求,都在后面某篇找到了代码归宿——需求驱动开发;
- 02 篇的三个"不用"(ORM/鉴权库/前端框架),每处都省出了真实的安全与维护收益——选型是取舍不是站队;
- 05/07/08 篇反复出现的"判定收敛、终态保证、纵深防御"——工程可靠性的本质是纪律的可执行;
- 07/11 篇的评估驱动——效果类系统的进步必须可测量;
- 全仓库 300+ 行注释,大半在写"为什么"而不是"是什么"——注释是写给三个月后的自己和下一个接手的人的。
下一步你可以:给平台加一个自己的工作流节点、接一个真实大模型跑通业务、或者按 Roadmap 认领一项(PostgreSQL 适配、Redis 限流、向量检索融合)。仓库的 CONTRIBUTING.md 等着你:python -m pytest tests -q 全绿,欢迎 PR。
祝搭建顺利。有任何一步卡住,回到对应篇目的"常见坑"章节,或者在仓库提 Issue。