内容
# 自托管文档栈
一个基于数据库的文档控制面板,您可以在自己的基础设施上运行。
这是运行 `docs.charliehub.net` 的通用、无内容版本的栈 — 共享给您,以便您可以使用自己的 PostgreSQL、Python 和 Web 服务器建立相同的服务。
**您将获得:**
- **docs-api** — 一个 FastAPI 服务,存储每个文档页面在 PostgreSQL 中(单一真实来源),具有乐观锁定、完整版本历史、回滚、软删除/恢复、页面移动和自动重定向、导航树管理、 lint 规则、漂移检测和 Prometheus 指标。
- **MkDocs + nginx** — API 将页面渲染到磁盘,构建一个 [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) 站点在暂存目录中,并将其原子化地同步到 nginx 服务的目录中(在重建过程中没有 404 窗口)。原始 Markdown 也在 `/raw/` 下提供,以便代理可以直接获取页面源。
- **docs-mcp** — 一个 MCP(模型上下文协议)服务器,作为文档站点的前端,供 AI 代理(Claude Code、Claude Desktop、任何具有 MCP 能力的代理)能够搜索、读取、写入和存档文档页面作为一流工具。
- **claude-md** — 一种模式(和一个现成的同步脚本),用于从存储在文档数据库中的页面编译每台机器的 `CLAUDE.md` 代理上下文文件,以便代理上下文是版本化的、可审计的和集中管理的。
**故意不包含的内容:** 任何文档内容、机密、密钥、主机名或特定于站点的配置。这些由您自行提供。
---
## 架构
```
代理 / 脚本 / 人类 / MCP 客户端
│ PUT /api/docs/pages/{path} ┌───────────────┐
▼ │ docs-mcp │
┌──────────┐ 真实来源 │ (MCP 服务器) │
│ docs-api │ ◄──────────────► PostgreSQL └──────┬────────┘
│ (FastAPI)│ (docs 模式) │ 搜索索引 +
└────┬─────┘ │ 原始 Markdown(只读)
│ 部署(每次写入后自动触发,合并) │
▼ │
mkdocs/docs/*.md + mkdocs.nav.yml (生成的导航)│
▼ │
mkdocs 构建 → 暂存 → rsync(按文件原子化) │
▼ │
mkdocs/site/ ◄────────────────────────────────────┘
▼
nginx (docs-web)→ 您的反向代理 → 读取者
```
关键不变性(整个设计依赖于这些):
1. **PostgreSQL 是唯一的真实来源。** 磁盘上的 Markdown 文件是构建产物,每次部署时都会重新生成。永远不要直接编辑它们 — API 会检测漂移并覆盖它。
2. **所有写入都通过 API 进行。** 这为您提供了版本历史、乐观锁定(`If-Match`)、验证和审计标签。
3. **生成的导航存储在 `mkdocs.nav.yml` 中** (git 忽略,每次部署重写),它继承了手动维护的 `mkdocs/mkdocs.yml`。您可以编辑主题/插件;API 拥有导航和重定向映射。
## 先决条件
- 带有 Compose v2 的 Docker
- ~1 GB 磁盘空间用于镜像 + 您的内容
- 主机上的 `curl`、`jq`、`openssl` 用于辅助脚本
## 快速入门
```bash
# 1. 配置机密(两个 API 密钥 + 一个数据库密码)
cp .env.example .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$(openssl rand -hex 24)/" .env
sed -i "s/^DOCS_API_KEY=.*/DOCS_API_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/^MCP_API_KEY=.*/MCP_API_KEY=$(openssl rand -hex 32)/" .env
# 2. 创建容器挂载的工作目录(docs-api 以 uid 1000 运行
# 并在此处写入渲染的 Markdown + 构建的站点;如果您的主机用户不同,
# 请在 docker-compose.yml 中调整 uid)
mkdir -p mkdocs/docs mkdocs/site
sudo chown -R 1000:1000 mkdocs
# 3. 构建并启动
docker compose up -d --build
# 4. 种子首页 + 示例页面,触发第一次部署
./scripts/seed-example-docs.sh
# 5. 浏览
open http://localhost:8080 # 文档站点
open http://localhost:8010/docs # docs-api OpenAPI UI
```
> **推荐部署:仅限内部使用。** 此栈设计用于在您的 LAN 或 VPN(WireGuard/Tailscale)上运行,而不是作为公共互联网站点 — 上游部署就是这样运行的。所有端口都绑定到 `127.0.0.1`;要服务于 localhost 之外,请在前面放置一个反向代理(Caddy、Traefik、nginx)并启用 TLS **,并使其仅可从您的私有网络访问**。如果您确实需要公开访问,请先阅读 [SECURITY.md](SECURITY.md) 并在站点前面添加 SSO 或另一个身份验证层 — 文档页面本身没有身份验证。
## 编写页面
所有内容都是一个 `{title, nav_path, content}` 的 `PUT` 操作到一个路径:
```bash
source .env
# 创建
curl -sf -X PUT http://localhost:8010/api/docs/pages/guides/backups.md \
-H "X-API-Key: $DOCS_API_KEY" -H "Content-Type: application/json" \
-d '{"title":"Backups","nav_path":"Guides/Backups","content":"# Backups\n\nHow we back things up."}'
# 更新(乐观锁定:读取修订,发送它回来作为 If-Match;
# 或使用 If-Match: * 显式使用最后写入者获胜)
REV=$(curl -sf -H "X-API-Key: $DOCS_API_KEY" \
http://localhost:8010/api/docs/pages/guides/backups.md | jq -r .revision)
curl -sf -X PUT http://localhost:8010/api/docs/pages/guides/backups.md \
-H "X-API-Key: $DOCS_API_KEY" -H "If-Match: \"$REV\"" \
-H "Content-Type: application/json" \
-d '{"title":"Backups","nav_path":"Guides/Backups","content":"# Backups\n\nUpdated."}'
```
- `path` 是页面的 URL 标识(`guides/backups.md` → `/guides/backups/`);小写 `[a-z0-9/_-]` 加 `.md`。
- `nav_path` 是它在侧边栏中的位置(`Guides/Backups`)。路径和导航是故意解耦的;API 会警告(并在明显情况下阻止)写入,看起来像是该部分是从 URL 机械生成的。
- 每次写入都会自动触发合并的后台部署(〜秒)。
有用的端点(所有都在 `http://localhost:8010` 下,所有 `X-API-Key` 认证):
| 端点 | 目的 |
|----------|---------|
| `GET /api/docs/pages` | 列出页面(`?status=active\|archived\|all`) |
| `GET /api/docs/pages/{path}` | 读取一个页面(+ `ETag` 修订版) |
| `PUT /api/docs/pages/{path}` | 创建/更新 |
| `POST /api/docs/pages/{path}/archive` | 软删除(可恢复) |
| `POST /api/docs/pages/{path}/restore` | 恢复已存档的页面 |
| `GET /api/docs/pages/{path}/history` | 版本历史 |
| `POST /api/docs/pages/{path}/rollback/{revision}` | 回滚 |
| `POST /api/docs/pages/move` | 移动页面的 URL,并带有自动重定向 |
| `GET /api/docs/nav` | 渲染的导航树 + 冲突报告 |
| `POST /api/docs/nav/reparent` | 批量移动侧边栏子树 |
| `PUT /api/docs/nav/sections` | 订购顶级部分 |
| `POST /api/docs/deploy` | 强制重建(`?dry_run=true` 预览) |
| `GET /api/docs/drift` | 数据库与磁盘漂移报告 |
| `GET /api/docs/lint` | 内容 lint 发现 |
| `GET /api/docs/search?q=…` | 全文搜索 |
| `GET /metrics` | Prometheus 指标(漂移,导航分歧) |
在 `http://localhost:8010/docs` (OpenAPI UI)上探索其余部分,并阅读 **[API_GUIDE.md](API_GUIDE.md)** 以获取完整的写入路径语义:`If-Match` 模式、导航部分防护、移动/重定向机制、文档生命周期模型和渲染问题。种子脚本将该指南发布到您的站点中,作为 `guides/docs-api.md`,因此您的文档是自我文档化的。
## 连接 AI 代理(MCP)
`docs-mcp` 通过 `http://localhost:8049/mcp` 上的流式 HTTP 将文档作为 MCP 工具暴露出来,使用 bearer-token 认证:
- `search_docs(query)` — 排名关键字搜索(从站点构建的 Lunr 索引)
- `read_doc(path_or_slug)` — 页面的完整 Markdown 源
- `list_docs()` / `write_doc(...)` / `archive_doc(path)` — 完整的写入路径
- `resolve_concept(term)` — 您的项目术语 → 规范概念 + 文档链接(编辑 `mcp/aliases.yml`,热加载)
克劳德代码:
```bash
source .env
claude mcp add docs --transport http http://localhost:8049/mcp \
--header "Authorization: Bearer $MCP_API_KEY"
```
每次部署后,docs-api 都会在 `docs-mcp:/reindex` 上触发一个 webhook,以便搜索索引在内容更改后几秒钟内跟随。
## CLAUDE.md 模式
如果您在几台机器上运行编码代理,保持每个机器的 `CLAUDE.md` (或 `AGENTS.md` )最新是一个容易腐烂的状态。我们使用的模式:将共享上下文和每个机器的节点特定上下文存储为文档数据库中的普通页面,并让每个机器上的一个小型定时器驱动的脚本组成并安装只读文件。参见 [claude-md/README.md](claude-md/README.md) — 包含同步脚本和 systemd 单元。
## 操作它
- **备份:** 数据库是一切。`docker compose exec postgres pg_dump -U docs docs > backup.sql` 在 cron 上;站点从中重建。
- **漂移:** `GET /api/docs/drift` 告诉您是否有人在 API 背后编辑了渲染的文件(无论如何,它们都会在下一次部署时被覆盖)。
- **监控:** 使用 Prometheus 抓取 `docs-api:/metrics`(如果您有的话)。
- **升级:** 提升 `docs-api/requirements.txt` 中的销钉(保持 `mkdocs-material` 与其他固定销钉同步),重建。
## 自定义
- **主题/插件:** 编辑 `mkdocs/mkdocs.yml` (手动维护;导航和 `redirect_maps` 块由 API 拥有 — 单独留下它们)。
- **部分顺序:** 使用您的有序列表 `PUT /api/docs/nav/sections`,或编辑 `docs-api/app/generator.py` 中的 `DEFAULT_SECTION_ORDER` 引导。
- **Lint 规则:** `docs-api/app/lint_rules.py` 带有 *我们的* 规则作为示例(过时的 CIDR、已退休的容器名称…)。用编码 **您的** 基础设施的过去事件的检查替换它们。
- **术语解析器:** 复制 `mcp/aliases.example.yml` 到 `mcp/aliases.yml` 并添加您团队的词汇。
## 仓库布局
```
docker-compose.yml postgres + docs-api + docs-web(nginx)+ docs-mcp
.env.example 三个机密 + 公共 URL
db/init.sql 模式(docs.pages, docs.page_versions, docs.sections)
docs-api/ FastAPI 控制面板(从上游复制,未修改)
mkdocs/ mkdocs.yml 模板,构建钩子,nginx 配置,主题覆盖
mcp/ MCP 服务器(仅限文档工具)
claude-md/ CLAUDE.md 分发模式 + 同步脚本 + systemd 单元
scripts/ seed-example-docs.sh, sync-from-upstream.sh
```
## 将其用作独立仓库
此目录是自包含的。要提升它:
```bash
cp -r docs-stack-selfhost ~/my-docs-stack && cd ~/my-docs-stack && git init
# 或,从父仓库保留历史:
git subtree split -P docs-stack-selfhost -b docs-stack && git push <newremote> docs-stack:main
```
如果您从上游仓库收到此目录,`scripts/sync-from-upstream.sh` 会刷新 `docs-api/` 副本从规范源。