1. 为什么第二大脑需要 Docker 化部署
如果你已经跟着前两篇把 Hermes Agent 和 Obsidian 的联动跑通了,大概率会遇到一个尴尬:本地笔记本上跑得好好的,换台机器就各种依赖报错;或者你想让 Agent 7×24 小时盯着 Vault 做索引、跑摘要,总不能一直开着笔记本。这时候 Docker 部署就不是「炫技」,而是把第二大脑从玩具变成基础设施的必经一步。
Hermes Agent 本质上是一个「常驻服务 + 文件监听 + 模型调用」的组合体。它需要 Python 运行时、Node 前端、PostgreSQL 存元数据、Redis 做任务队列,还要能读写你的 Obsidian Vault 目录。手工装这一套,光是 Python 版本和 psycopg2 编译就能耗掉你一个下午。Docker Compose 把这些组件打包成互相隔离又互相连通的容器,一条docker compose up -d就能拉起整套环境,升级时docker compose pull换镜像即可,回滚也只是换个 tag。
这篇是「Hermes Agent + Obsidian 打造第二大脑」系列的第三篇,聚焦 Docker 生产级部署。我会从镜像拉取、数据卷与网络规划讲起,给出可直接复制的docker-compose.yml和.env配置,再逐步验证请求、排查常见报错。模型服务这一层,我用 TaoToken 统一 Key 和 API 通道接入,这样 OpenAI、Anthropic、本地 Ollama 都能走同一个入口,省得在多个平台之间来回切换配置。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,后面配置里会具体写怎么填 Base URL 和 Model ID。
适合谁看:已经有一台能跑 Docker 的服务器(本地 NAS、云主机、家里的迷你主机都行),想让 Obsidian 第二大脑具备自动索引、自动摘要、跨设备同步能力的同学。如果你还没装 Docker,先执行curl -fsSL https://get.docker.com | sh把 Docker 和 Compose 插件装上,再往下走。
2. TaoToken 前置准备:统一 Key 与 API 通道
在写 compose 文件之前,先把模型服务这一层理清楚。Hermes Agent 的 API 容器和 Worker 容器都需要调用大模型,如果每个容器里塞不同的厂商 Key,配置会散得到处都是,换模型时改到崩溃。我的做法是:所有模型请求统一走 TaoToken 的 API 通道,容器里只认一个 Base URL 和一个 Key。
TaoToken 在这里扮演的是「统一模型网关」的角色。你不需要在 Hermes 里为 OpenAI、Anthropic 各写一套适配,只要把OPENAI_API_BASE指向 TaoToken 的 API 地址,把OPENAI_API_KEY填成 TaoToken 生成的 Key,模型名换成对应的 Model ID 就行。这样 Hermes 的代码逻辑完全不用动,切换模型只是改一个环境变量。
具体操作路径是这样的:先到控制台创建一个 API Key,然后确认你要用的模型 ID。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完 Key 之后先别关页面,复制下来,后面.env里要用。
这里有个容易踩的坑:很多人以为 Base URL 要填到/v1/chat/completions这一层,其实不用。Hermes 内部用的是 OpenAI SDK 风格,Base URL 填到/v1就够了,SDK 会自己拼路径。TaoToken 的 API 根地址是 https://taotoken.net/api ,所以环境变量里写https://taotoken.net/api/v1即可。如果你用的是 Anthropic 风格的调用,Hermes 也支持单独配ANTHROPIC_BASE_URL,同样指向 TaoToken 的对应端点。
模型 ID 这块,建议先在模型对话页面确认一下你要用的模型能不能正常响应。模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,随便发一句「你好」看返回是否正常。确认没问题后,把模型 ID 记下来,比如gpt-4o、claude-3-5-sonnet-20241022这类。Hermes 的.env里OPENAI_MODEL就填这个值。
如果你打算长期跑 Agent 任务,比如让 Hermes 定时扫描 Vault 做摘要、建索引,那 Coding Plan 会更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它的额度模型更适合这种持续调用的场景,不用每次按量计费心里没底。
把 Key 和 Model ID 准备好之后,我们就可以进入 compose 配置环节了。记住三个东西:Base URL 是https://taotoken.net/api/v1,Key 是刚复制的那串,Model ID 是你确认过能用的那个。这三件套在后面的.env和docker-compose.yml里都会出现。
3. 可复制的 docker-compose 与 .env 配置
这一节是全文的核心,配置能直接复制粘贴用。先建工作目录:
mkdir -p ~/hermes-agent && cd ~/hermes-agent mkdir -p data/vault data/postgres data/redis data/backups logs nginx/conf.d然后创建.env文件。注意这个文件不要提交到 Git,里面全是敏感信息:
# ============ 模型服务(TaoToken 统一通道)============ OPENAI_API_KEY=sk-你的TaoToken密钥 OPENAI_API_BASE=https://taotoken.net/api/v1 OPENAI_MODEL=gpt-4o OPENAI_MAX_TOKENS=4096 OPENAI_TEMPERATURE=0.7 # 如果要用 Anthropic 风格 ANTHROPIC_API_KEY=sk-你的TaoToken密钥 ANTHROPIC_BASE_URL=https://taotoken.net/api ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # ============ 安全密钥(用 openssl rand -base64 32 生成)============ JWT_SECRET=替换成随机32字节 API_SECRET_KEY=替换成随机32字节 # ============ Obsidian Vault ============ OBSIDIAN_VAULT_PATH=/data/vault # ============ 数据库 ============ POSTGRES_DB=hermes POSTGRES_USER=hermes POSTGRES_PASSWORD=替换成强密码 POSTGRES_HOST=postgres POSTGRES_PORT=5432 # ============ Redis ============ REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD=替换成强密码 # ============ 同步与 Worker ============ SYNC_INTERVAL=30 SYNC_STRATEGY=keep_both WORKER_CONCURRENCY=5 WORKER_QUEUE=hermes_tasks # ============ 日志 ============ LOG_LEVEL=info LOG_FORMAT=json # ============ 端口 ============ WEB_PORT=3000 API_PORT=8080生成随机密钥的命令:
openssl rand -base64 32 # JWT_SECRET openssl rand -hex 32 # API_SECRET_KEY接下来是docker-compose.yml。这份配置把 API、Web、Worker、PostgreSQL、Redis 五个服务编排在一起,网络用自定义 bridge,数据卷全部挂到宿主机目录,方便备份和迁移:
services: postgres: image: postgres:16-alpine container_name: hermes-postgres restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - ./data/postgres:/var/lib/postgresql/data networks: - hermes-network healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 start_period: 30s redis: image: redis:7-alpine container_name: hermes-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes volumes: - ./data/redis:/data networks: - hermes-network healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 api: image: hermes/hermes-api:latest container_name: hermes-api restart: unless-stopped env_file: .env environment: - POSTGRES_HOST=postgres - REDIS_HOST=redis - OBSIDIAN_VAULT_PATH=/data/vault ports: - "${API_PORT}:8080" volumes: - ./data/vault:/data/vault - ./logs:/app/logs depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - hermes-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 15s timeout: 5s retries: 3 start_period: 40s worker: image: hermes/hermes-worker:latest container_name: hermes-worker restart: unless-stopped env_file: .env environment: - POSTGRES_HOST=postgres - REDIS_HOST=redis - OBSIDIAN_VAULT_PATH=/data/vault volumes: - ./data/vault:/data/vault - ./logs:/app/logs depends_on: api: condition: service_healthy networks: - hermes-network web: image: hermes/hermes-web:latest container_name: hermes-web restart: unless-stopped env_file: .env environment: - API_BASE_URL=http://api:8080 ports: - "${WEB_PORT}:3000" depends_on: api: condition: service_healthy networks: - hermes-network networks: hermes-network: driver: bridge几个关键点解释一下。depends_on配合condition: service_healthy保证启动顺序:PostgreSQL 和 Redis 先健康,API 才启动;API 健康后 Worker 和 Web 才启动。这样避免「数据库还没起来 API 就报连接失败」的经典问题。数据卷全部用./data/xxx绑定挂载,而不是命名卷,好处是你ls data/就能看到所有持久化数据,备份时直接tar打包即可。
Vault 目录挂载是重点:./data/vault:/data/vault,容器内路径和.env里的OBSIDIAN_VAULT_PATH保持一致。你本地的 Obsidian Vault 可以软链过来,或者直接把 Vault 目录放到data/vault下。权限问题后面第五节会专门讲。
启动命令:
docker compose pull docker compose up -d docker compose ps预期看到五个容器都是Up状态,其中 postgres、redis、api 显示(healthy)。如果 api 一直starting,等 40 秒左右再看,健康检查有start_period。
4. 验证请求与成功结果
容器起来不代表服务能用,得实际打几个请求确认。先看健康检查:
curl http://localhost:8080/health正常返回类似:
{ "status": "ok", "version": "1.0.0", "services": { "database": "connected", "redis": "connected", "vault": "accessible" } }如果database是disconnected,说明 API 连不上 PostgreSQL,去查docker compose logs api | grep -i postgres。如果vault是inaccessible,是挂载或权限问题,看第五节。
接着验证模型通道是否打通。这一步很关键,因为 TaoToken 的 Base URL 或 Key 填错的话,健康检查是看不出来的,只有实际调用才会暴露:
curl -X POST http://localhost:8080/api/v1/chat \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $API_SECRET_KEY" \ -d '{"message": "用一句话介绍 Obsidian", "model": "gpt-4o"}'预期返回:
{ "status": "success", "reply": "Obsidian 是一款基于本地 Markdown 文件的双向链接笔记工具。", "usage": {"prompt_tokens": 18, "completion_tokens": 24} }如果返回 401,是API_SECRET_KEY没对上;如果返回model not found,是OPENAI_MODEL填错了,去模型对话页面确认一下模型 ID。如果返回连接超时,检查OPENAI_API_BASE是不是https://taotoken.net/api/v1,注意结尾不要多斜杠。
再验证 Vault 读写。在宿主机data/vault下建个测试文件:
echo "# 测试笔记" > data/vault/test-sync.md docker compose exec api ls -la /data/vault/应该能看到test-sync.md。然后触发一次同步:
curl -X POST http://localhost:8080/api/v1/sync/trigger \ -H "Authorization: Bearer $API_SECRET_KEY"返回{"status":"success","files_processed":1}就说明文件监听和索引链路通了。最后打开浏览器访问http://你的服务器IP:3000,应该能看到 Hermes 的 Web 界面,首次访问会让你创建管理员账号。创建完登录进去,在 Vault 列表里能看到刚才的测试文件,整个部署就算验证通过了。
5. 本篇常见报错排查
部署过程中最容易撞上的几个报错,我按出现频率排一下。
401 Unauthorized。这个分两种:一种是调 Hermes 自己的 API 时 401,说明API_SECRET_KEY和请求头里的 Bearer 不一致,检查.env和 curl 命令;另一种是 Hermes 调 TaoToken 时 401,说明OPENAI_API_KEY填错了或者过期了,去 API Key 管理页重新生成一个。注意.env改完要docker compose up -d重建容器才生效,光restart不会重新读 env_file。
local proxy failed / connection refused。这个报错通常出现在 API 容器启动时,日志里会写dial tcp 127.0.0.1:5432: connect: connection refused。原因是POSTGRES_HOST被写成了localhost或127.0.0.1。容器里的 localhost 是容器自己,不是宿主机,更不是 postgres 容器。正确值应该是postgres,也就是 compose 里的服务名。同理 Redis 的 host 是redis。如果你在.env里写了POSTGRES_HOST=localhost,compose 的environment段会覆盖它,但如果你把覆盖那段删了就会踩坑。
reading choices: unexpected end of JSON input。这个报错来自模型调用层,意思是 TaoToken 返回的响应体不是合法 JSON。常见原因有三个:Base URL 写成了https://taotoken.net/api但漏了/v1,导致请求打到了错误路径;Model ID 填了一个不存在的模型,网关返回了 HTML 错误页;或者请求体里max_tokens设得过大超过了模型上限。排查方法是在宿主机直接 curl 一下 TaoToken:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'如果这条能通,说明 Key 和 Base URL 没问题,问题在 Hermes 的配置映射上。
OAuth / token refresh failed。如果你用的是 Anthropic 风格接入,Hermes 可能会尝试走 OAuth 流程,但 TaoToken 走的是 API Key 模式,不需要 OAuth。解决办法是在.env里显式设置ANTHROPIC_AUTH_MODE=api_key,或者干脆只用 OpenAI 兼容模式,把LLM_PROVIDER设成openai。这样 Hermes 不会去尝试 OAuth 刷新。
Vault 权限拒绝。日志里出现permission denied或EACCES,是因为容器内进程的 UID 和宿主机目录所有者不匹配。先查容器用户:
docker compose exec api id假设输出uid=1000(hermes) gid=1000(hermes),那就把宿主机 Vault 目录改成这个属主:
sudo chown -R 1000:1000 data/vault chmod -R 755 data/vault如果你在 macOS 或 WSL2 上跑,文件系统权限映射比较特殊,直接chmod -R 777 data/vault也能绕过,但生产环境不建议这么干。
容器反复重启。docker compose ps看到Restarting状态,先看日志:
docker compose logs --tail 100 api如果是OOMKilled,说明内存不够,在 compose 里给 api 加deploy.resources.limits.memory: 2G。如果是exit code 1且日志只有一行,多半是.env里有语法错误,比如值里带了空格没加引号。
6. 长期运行与后续接入建议
部署跑通只是起点,真正让第二大脑产生价值的是它持续在后台工作。这里给几个实操建议。
第一,把备份做成定时任务。Vault 是你的核心资产,PostgreSQL 里存的是索引和元数据,两者都要备。写个脚本每天凌晨跑:
#!/bin/bash DATE=$(date +%Y%m%d) docker compose exec -T postgres pg_dump -U hermes hermes | gzip > data/backups/db_$DATE.sql.gz tar -czf data/backups/vault_$DATE.tar.gz -C data vault find data/backups -mtime +30 -delete加到 crontab:0 3 * * * ~/hermes-agent/backup.sh。
第二,日志要轮转。LOG_FORMAT=json配合 Docker 的日志驱动,在 compose 里给每个服务加:
logging: driver: json-file options: max-size: "50m" max-file: "5"不然跑几个月日志能把磁盘撑满。
第三,模型通道的长期策略。如果你只是偶尔用,按量计费没问题;但如果 Hermes 要跑定时摘要、自动打标签这类持续任务,建议看一下 Coding Plan,额度模型更适合这种场景,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言 SDK 的示例,如果你要自己写脚本调模型,可以参考。
第四,后续扩展方向。当前是单机 compose 部署,如果 Vault 变大、索引变慢,可以先把 PostgreSQL 的shared_buffers调大,或者把 Worker 的WORKER_CONCURRENCY从 5 提到 10。再往上就是多副本 + Nginx 负载均衡,不过对个人第二大脑来说,单机跑个一两年完全够用。真要上 K8s,那是团队协作场景才需要考虑的事。
最后提醒一句:.env文件千万别提交到 Git,.gitignore里加上.env、data/、logs/。我见过有人把带 Key 的配置推到公开仓库,结果被扫到后额度被刷光。部署这件事,稳比快重要,配置一次跑通,后面就是让它安静地在后台干活。