[项目复盘]从本地Python项目到双飞书AI机器人:Hermes Agent + DeepSeek的Docker 化与云端部署实战
一次面向真实客户需求的轻量生产实践:容器化、云端部署、数据持久化、飞书长连接、多实例隔离与自动备份。前言
这次项目最初看起来并不复杂:把已经能在本地运行的 Hermes Agent 接入 DeepSeek,再交付给客户使用。但真正开始考虑交付以后,问题很快从“程序能不能运行”变成了:Docker 容器删除或升级后,长期记忆会不会丢失?本地 Mac 构建的环境能不能迁移到 Linux 服务器?两个机器人能不能使用相同能力,但不共享聊天记录和长期记忆?这些问题本质上已经不是简单的 Python 项目安装,而是一次小型 AI Agent 生产化改造。本文完整记录我如何将本地运行的 Hermes Agent:本文不会展示任何真实服务器地址、App ID、App Secret 或 API Key。文中的敏感值均使用占位符。
一、先澄清一个概念:DeepSeek 并没有部署在服务器上
准确来说,这个项目不是“把 Hermes 和 DeepSeek 都部署到服务器”,而是:将 Hermes Agent 部署到云服务器,并由 Hermes 通过 HTTPS 调用 DeepSeek API。
飞书用户 ↓飞书开放平台 ↓ WebSocket 长连接云服务器上的 Hermes Agent ↓ HTTPS APIDeepSeek ↓Hermes 处理会话、记忆与工具调用 ↓飞书机器人回复用户
DeepSeek 的模型推理发生在 DeepSeek 服务端,因此阿里云服务器不需要 GPU。服务器主要承担:飞书消息接入Hermes Agent 运行,会话管理,长期记忆,Skills 和工具调用,数据库与文件持久化,定时任务和备份。这也是为什么一台普通的 CPU 云服务器就可以运行这个方案。
二、需求分析:为什么最终选择云端部署
1. 最初考虑过 Windows 一键启动包
客户已经安装 Docker Desktop,因此一开始考虑制作 Windows 一键启动包,让客户:这个方案可以运行,但很快暴露出几个交付层面的问题:Windows PowerShell 版本和脚本编码可能不一致;实际测试中,PowerShell 脚本还遇到过中文编码导致的语法解析错误。这个问题可以通过 UTF-8 BOM、纯英文脚本和 CRLF/LF 管理解决,但它提醒我:客户端电脑不应该成为机器人服务端。2. 最终采用云端常驻方案
客户电脑 / 手机 ↓ 飞书 ↓阿里云 ECS 上的 Hermes ↓ DeepSeek API
客户电脑关机不影响机器人、客户重装系统后只需要重新安装并登录飞书;对于单客户、低并发场景,这是比“每台客户电脑部署一套 Agent”更合适的方案。
三、阶段一:检查 Hermes 的真实运行方式
在修改项目之前,我没有直接凭经验编写 Dockerfile,而是先检查:实际启动命令、Python 版本、依赖管理方式、配置文件位置、DeepSeek API Key 的读取位置;会话、长期记忆和数据库的位置;Skills、Prompt 和工具配置的位置;是否存在官方 Dockerfile;是否依赖 Apple Notes、桌面环境或 computer_use;是否存在写死的 macOS 本机路径。这个步骤非常重要,因为 Agent 项目和普通 Web 服务不同。它通常不仅有代码,还会在用户目录保存会话数据库,用户画像,长期记忆,Skills,浏览器状态,定时任务,工具凭证,自定义系统提示词。如果只把源代码放进 Docker,却没有找到这些运行时数据,容器看起来可以启动,但重建以后记忆会全部消失。在本项目中,最终将 Hermes 的运行时主目录统一映射为:容器内:/workspace宿主机:./workspace
容器内:/backups宿主机:./backups
四、接入 DeepSeek API
DeepSeek 通过兼容接口接入 Hermes。核心原则是:密钥只通过环境变量传递,不进入代码、Dockerfile、Compose 文件或镜像层。model: provider: custom base_url: https://api.deepseek.com default: <实际使用的模型名称> api_mode: chat_completions
接入后先在本地执行一次最小测试,确认 Hermes 能通过 DeepSeek 返回普通文本,再进入 Docker 化阶段。这样可以把“模型配置问题”和“容器问题”分开排查。
五、阶段二:Docker 化
1. 不复制本机虚拟环境
本地开发机器是 Apple Silicon Mac,目标服务器是 Linux AMD64。正确做法是在镜像内部,根据 pyproject.toml 和 uv.lock 重新安装依赖。2. 生产镜像的主要设计
支持 linux/amd64 和 linux/arm64;使用 s6-overlay 管理 Hermes 主进程和 Gateway;容器启动时,PID 1 由进程监督系统负责;具体 Hermes 服务降权到非 root 用户运行。这比直接让 Python 进程作为 PID 1 更适合长期运行。3. Docker Compose 编排
services: hermes: image: /hermes-agent: platform: linux/amd64 restart: unless-stopped env_file: - ./data/.env environment: HERMES_HOME: /opt/data HERMES_UID: ”10000” HERMES_GID: ”10000” working_dir: /workspace volumes: - ./data:/opt/data - ./workspace:/workspace - ./backups:/backups healthcheck: test: - CMD-SHELL - /command/s6-svstat /run/service/gateway-default | grep -q '^up ' interval: 30s timeout: 10s retries: 5 logging: driver: json-file options: max-size: 10m max-file: ”5” command: [”gateway”, ”run”]
这里有几个刻意的安全选择:没有使用 privileged: true、没有挂载宿主机 Docker Socket、没有挂载整个用户主目录、没有为了 computer_use 开放桌面权限、没有默认暴露端口、没有把密钥直接写进 Compose。4. 为什么没有开放端口
飞书采用 WebSocket 长连接模式,由 Hermes 主动连接飞书服务器。不开放无关公网端口可以减少扫描攻击,后台未授权访问,TLS 和域名维护,与服务器现有 Nginx 服务的端口冲突。如果未来启用 Webhook、Dashboard 或 API,再通过单独的 Compose override 和反向代理显式开放。
六、长期记忆与数据持久化
容器本身应当被视为可随时删除的临时计算单元,不能把重要数据只保存在容器可写层。data/├── .env├── config.yaml├── SOUL.md├── state.db├── memories/├── sessions/├── pairing/├── platforms/├── skills/├── cron/├── hooks/└── logs/
这样可以保证:docker compose stop 后数据不丢失、docker compose down 后数据不丢失、删除并重建容器后数据不丢失、更新镜像后数据不丢失、服务器迁移时可以复制或恢复数据。我还进行了容器删除和重新创建测试:先写入测试记忆,再停止并删除容器,重新创建后验证该信息仍然可用。这个测试比单纯检查目录是否存在更有意义,因为它验证了业务层确实读取了持久化数据。
七、备份与恢复
1. 备份内容
长期记忆、会话记录、state.db、.env、config.yaml、SOUL.md、Skills、Cron、Hooks、平台配对数据、Compose 和部署脚本、依赖锁文件。2. 完整性校验
HERMES_BACKUP_PASSPHRASE='' \ ./scripts/backup.sh
恢复脚本在覆盖数据前会:检查归档路径穿越;拒绝符号链接和设备文件;校验 SHA-256;检查状态 ZIP 完整性;检查记忆、Skills、数据库和配置是否存在;要求人工输入 RESTORE 确认;先备份当前状态,再执行覆盖。3. 自动备份
云服务器使用 systemd timer 每日执行备份,并为两个机器人设置不同时间,避免同时占用磁盘和 CPU。需要说明的是:保存在同一台服务器上的备份只能防止误操作,不能替代异地灾备。更成熟的做法是将加密备份同步至:阿里云 OSS、客户控制的对象存储、另一台服务器、离线加密介质。
八、构建镜像并发布到私有仓库
在 Apple Silicon Mac 上可以构建 AMD64 镜像:docker buildx build \ --platform linux/amd64 \ -t /hermes-agent:-amd64 \ --load .
docker logindocker push /hermes-agent:-amd64
docker buildx build \ --platform linux/amd64,linux/arm64 \ -t /hermes-agent: \ --push .
这里使用私有仓库的原因是:镜像包含业务代码;可以控制拉取权限;可以使用明确版本标签;服务器不需要现场编译全部依赖;升级和回滚更简单。不建议使用不固定的 latest 作为生产部署版本。更稳妥的方式是使用明确版本,进一步还可以锁定镜像 digest。
九、部署到阿里云 ECS
服务器准备工作包括:安装 Docker Engine;安装 Docker Compose v2;配置约 2GB Swap;创建部署目录;登录私有镜像仓库;上传 Compose、脚本和配置模板;创建权限为 600 的 .env;拉取并启动镜像。/opt/hermes/├── compose.yaml├── data/├── workspace/├── backups/├── scripts/└── systemd/
cd /opt/hermesdocker compose up -d
docker compose psdocker compose logs -f hermes
十、接入第一个飞书机器人
1. 创建企业自建应用
在飞书开放平台创建企业自建应用,并添加“机器人”能力。application:bot.basic_info:readim:message.group_at_msg:readonlyim:message.p2p_msg:readonlyim:message:send_as_botim:resourceim:chat:readonly
根据实际功能还可以增加群组信息等权限,但应遵循最小权限原则,不要一次性授权与业务无关的会议、菜单或管理权限。2. 配置事件
3. 将凭证写入服务器
FEISHU_APP_ID=FEISHU_APP_SECRET=FEISHU_DOMAIN=feishuFEISHU_CONNECTION_MODE=websocketFEISHU_ALLOWED_USERS=FEISHU_HOME_CHANNEL=FEISHU_ALLOW_BOTS=noneFEISHU_REQUIRE_MENTION=trueFEISHU_GROUP_POLICY=allowlist
docker compose up -d --force-recreate
[Feishu] Connected in websocket modefeishu connected
4. 用户配对
第一次给机器人发送消息时,Hermes 返回一次性配对码。docker exec -u hermes \ hermes pairing approve feishu
批准后,将用户 Open ID 写入 FEISHU_ALLOWED_USERS,用于私聊和群聊访问控制。同时设置当前私聊为 Home Channel,使定时任务和跨平台通知有默认投递位置。飞书 → Hermes → DeepSeek → Hermes → 飞书
十一、第二个机器人:相同能力,但不共享历史
再增加一个配置相同的机器人,但两个机器人不能共享历史记录。
如果只在同一个 Hermes 实例里增加第二组飞书凭证,虽然会话可能按 Chat ID 区分,但下面这些数据仍可能处在同一 Hermes Home 中:长期记忆、用户画像、Skills 状态、全局数据库、配对记录、Cron 和平台状态。因此我没有只做“第二个飞书账号”,而是部署了第二个完整 Hermes 实例。飞书机器人 A ↓Hermes 容器 A ↓/opt/hermes/data飞书机器人 B ↓Hermes 容器 B ↓/opt/hermes-video/data
两者不共享:会话、长期记忆、用户画像、数据库、飞书凭证、白名单、Home Channel、配对记录、工作目录、备份。第二个实例使用不同的 Compose Project Name、容器名称和宿主机目录,并设置独立内存上限。通过 docker inspect 检查挂载路径,确认两个容器分别指向:容器 A → /opt/hermes/data容器 B → /opt/hermes-video/data
而不是共享同一个 /opt/data 宿主机来源。
十二、资源规划
服务器为轻量规格,两个 Hermes 容器空闲时合计占用数百 MB 内存,但系统、Docker、日志及其他服务也会消耗资源。容器内存上限、Swap 上限、PID 数量限制、日志轮转、Chromium /dev/shm 大小、错开的备份时间。对于低并发、小规模使用,2 核 2GB 加 Swap 可以运行。如果需要并发浏览器任务、文件处理或多个 Agent 同时工作,建议:Swap 只能降低突发 OOM 的概率,不能替代真实内存。
十三、测试清单
1. 构建测试
2. 启动测试
docker compose up -ddocker compose ps
3. 日志测试
docker compose logs -f hermes
4. DeepSeek 测试
向 Hermes 发送普通消息,确认模型正常返回。5. 当前会话测试
告诉 Hermes 一条信息,在同一会话继续追问。6. 长期记忆持久化测试
7. 备份测试
8. 飞书测试
私聊收发、中文回复、用户配对、Home Channel、群聊中 @机器人、图片和文件、长连接断开重连。9. 多机器人隔离测试
分别向两个机器人写入不同信息,然后交叉询问,确认:10. 敏感信息检查
Git 历史、镜像历史、Docker 构建日志、容器日志、README、Compose、备份权限。确保没有真实 API Key、App Secret 和服务器密码。
十四、实际遇到的问题
1. Windows PowerShell 中文编码错误
早期 Windows 一键包中的 PowerShell 脚本因为编码与 PowerShell 版本不一致,出现中文字符被错误解析,进而触发引号、花括号和 & 运算符相关的连锁语法错误。脚本使用 UTF-8 BOM,避免在关键脚本中使用中文字符串,固定 CRLF/LF 策略,在真实 Windows PowerShell 5.1 环境测试,将服务端从客户电脑迁移到云服务器。最终云端方案从根本上减少了客户侧环境差异。2. 服务器访问默认 PyPI 较慢
飞书 SDK 首次启用时需要安装额外依赖,服务器访问默认源出现长时间等待。通过配置可用的 Python 镜像源后,依赖安装完成,Gateway 正常建立飞书连接。这个问题说明:生产镜像最好尽可能预装确定会使用的依赖,减少首次启动时对外部软件源的依赖。3. “容器健康”不等于“飞书可用”
容器健康检查只能证明 Gateway 进程处于运行状态,不能证明:飞书 WebSocket 已连接,DeepSeek API 可调用,用户权限正确,消息能正常回复。Docker Health、Gateway 日志、飞书连接日志、飞书端真实消息、DeepSeek 实际返回结果。4. 配对成功不等于群聊已授权
FEISHU_GROUP_POLICY、FEISHU_ALLOWED_USERS、是否 @机器人、飞书消息权限等条件影响。因此批准配对后,还应把用户 Open ID 写入明确的飞书白名单。
十五、安全复盘
已经做到:密钥不进入镜像、.env 不进入 Git;私有镜像仓库、非特权业务进程、不开放无关端口、不挂载 Docker Socket、不挂载整个主目录、容器资源限制、用户配对与白名单、备份权限限制、日志轮转。密钥只要曾经出现在聊天、截图或不受控日志中,就应当视为已经暴露并进行轮换。
十六、这套方案算不算“生产级”
这是一套适合单客户、低并发场景的轻量生产方案,架构思路正确,具备持久化、隔离、备份、健康检查和自动恢复能力,但不是企业级高可用架构。
它的优点是:简单、成本可控、容易维护、数据边界清晰、故障定位直接、不给客户电脑增加运维负担。它的限制是:单台 ECS 是单点、没有跨节点故障转移、本机备份不等于异地灾备、健康检查不是完整业务探针、资源规模适合低并发、仍需更完善的监控和密钥管理。在这个业务规模下,没有为了“看起来高级”而引入 Kubernetes、数据库集群和复杂服务网格,是一种有意识的工程取舍。专业并不意味着使用最多的技术,而是:选择与业务规模匹配的架构、明确数据放在哪里、知道故障以后如何恢复、清楚哪些能力已经实现、不夸大尚未实现的高可用和安全性。
十七、最终交付效果
她不需要:安装 Python、创建虚拟环境、启动 Hermes、启动 DeepSeek、运行 Docker Desktop、保持电脑开机、在重装系统后恢复机器人服务。机器人 A:通用 Hermes 助手机器人 B:视频文案助手
两个机器人使用相同的 Hermes 能力和 DeepSeek API,但各自拥有独立的会话、长期记忆、用户画像和备份。客户看到的是两个飞书联系人,背后则是两个相互隔离的云端 Agent 实例。
结语
这次项目真正有价值的部分,不是“成功运行了一个 Docker 容器”,而是完成了从本地实验到可交付系统的思维转换:对于小规模 AI Agent 项目,这种方案不复杂,却足够清晰、可维护,也为后续增加监控、异地备份、CI/CD 和高可用留下了空间。这也是我对“专业交付”的理解:不是堆砌技术,而是在合适的成本内,把运行、数据、安全、维护和恢复都考虑进去。