本文基于 M5 MacBook Air 32G 上使用 Colima 运行 Docker 虚拟机的实战经验,介绍如何在 Docker 中部署 Hermes Agent,解决数据持久化、权限隔离、工具环境保留等关键问题,并分享踩坑记录和最佳实践。

为什么选择 Docker 部署

Hermes Agent 依赖 Python 虚拟环境、Node.js 运行时、Playwright 浏览器以及大量系统级工具(ffmpeg、pandoc、ripgrep 等)。直接裸机安装会导致:

  • 依赖冲突:多个项目共用 Python/Node 版本时容易出错
  • 难以迁移:换服务器需要重新配置所有环境
  • 权限混乱:不同服务以不同用户运行,文件属主不一致

Docker 部署的核心优势:

优势 说明
环境隔离 所有依赖封装在镜像内,不污染宿主机
可复现 镜像 SHA256 校验,每次构建结果一致
数据分离 用户数据挂载在 /opt/data volume,与代码层完全解耦
权限可控 运行时以非 root 用户执行,最小化攻击面

核心架构设计

目录职责划分

Hermes Agent 的目录设计是整个部署方案的核心:

# 代码层(镜像内,只读)
/opt/hermes/          # 代码、venv、node_modules
/opt/hermes/.venv/    # Python 虚拟环境
/opt/hermes/bin/      # 可执行文件

# 数据层(Volume 挂载,持久化)
/opt/data/            # 用户数据、配置、sessions、skills
/opt/data/.local/bin  # agent 安装的 npm/pip 工具

关键设计:容器内 $HOME 设为 /opt/data,npm prefix(~/.local)自然落在持久化路径内,工具不会因重启而丢失。

ENV HERMES_HOME=/opt/data
ENV PATH="/opt/hermes/bin:/opt/hermes/.venv/bin:/opt/data/.local/bin:${PATH}"
VOLUME [ "/opt/data" ]

进程监督:s6-overlay

镜像使用 s6-overlay 作为 PID 1,替代早期的 tini:

  • 僵尸进程回收:s6-svscan 在 SIGCHLD 时非阻塞回收孤儿进程
  • 服务监督:main-hermes、dashboard、per-profile gateway 均受监督,崩溃自动重启
  • 启动顺序保证:cont-init.d 脚本按字典序执行,权限修复在服务启动前完成

非 root 运行

RUN useradd -u 10000 -m -d /opt/data hermes

每个监督服务通过 s6-setuidgid hermes 降权运行。docker exec 默认以 root 进入容器——为此提供了 /opt/hermes/bin/hermes shim,自动转发给 s6-setuidgid hermes 执行,避免写出 root 属主文件导致运行时 EACCES。

对官方 Dockerfile 的修改

1. 镜像源全面国内化

问题:官方镜像从 Docker Hub、deb.debian.org、npmjs.org、pypi.org 拉取,在国内构建速度极慢甚至超时。

解决方案

# Node 基础镜像:官方 → DaoCloud 镜像
- FROM node:22-bookworm-slim AS node_source
+ FROM docker.m.daocloud.io/library/node:22-bookworm-slim AS node_source

# apt 源:deb.debian.org → 中科大镜像
+ RUN sed -i 's|deb.debian.org|mirrors.ustc.edu.cn|g' /etc/apt/sources.list.d/debian.sources

# npm registry → npmmirror
+ ENV npm_config_registry=https://registry.npmmirror.com  # 比 npm config set 更早生效,构建阶段也生效

# PyPI → 清华镜像
+ RUN uv sync ... --index-url https://pypi.tuna.tsinghua.edu.cn/simple/

2. 新增系统工具包

问题:官方镜像为精简体积裁掉了部分工具,我们的使用场景需要它们。

+ wget          # 部分脚本依赖 wget 而非 curl
+ file jq       # 文件类型检测、JSON 处理
+ librsvg2-bin  # SVG 转换(cairosvg 的系统依赖)
+ pandoc        # 文档格式转换
+ g++ make cmake               # v0.17.0 Matrix gateway 依赖的原生编译工具
+ fonts-noto-cjk fonts-noto-cjk-extra  # 中文字体

3. 新增 Python 包

RUN uv pip install --no-cache-dir \
    --index-url https://pypi.tuna.tsinghua.edu.cn/simple/ \
    feedparser markdown whoosh \          # RSS 解析、Markdown 渲染、全文搜索
    Pillow numpy jieba \                  # 图像处理、数值计算、中文分词
    duckduckgo-search websocket-client \  # 搜索、WebSocket 通信
    cairosvg soundfile weasyprint \       # SVG/音频/HTML→PDF 转换
    wordcloud tiktoken pyarrow \          # 词云、token 计数、列式存储
    pytest litellm \                      # 测试框架、多模型 LLM 接口
    rtk-hermes                            # RTK Hermes 集成

4. rtk 固定版本(构建可复现)

ARG RTK_VERSION=0.42.4
RUN curl -fsSLk ... "https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-..."

不固定版本会在每次 docker build 时拉取最新 release,导致不同时间构建的镜像行为差异。ARG 固定后可通过 --build-arg RTK_VERSION=x.y.z 按需升级。

5. s6-overlay 下载方式优化

问题:官方用 curlRUN 步骤内下载 s6-overlay,每次构建都重新下载。

解决方案:改用 ADD 指令,BuildKit 会将 tarball 缓存在层中,仅 URL 变化时才重新拉取。

# 改为 ADD,利用 BuildKit 缓存
+ ADD https://github.com/.../s6-overlay-x86_64.tar.xz /tmp/s6-overlay-x86_64.tar.xz
+ ADD https://github.com/.../s6-overlay-aarch64.tar.xz /tmp/s6-overlay-aarch64.tar.xz
- curl -fsSL --retry 3 -o /tmp/s6-overlay-arch.tar.xz "..."

构建镜像

Dockerfile 修改完成后,先构建镜像再启动容器:

# 进入 Dockerfile 所在目录
cd /path/to/hermes-agent

# 构建镜像(首次约 10-15 分钟,后续有缓存会快很多)
docker build -t hermes-agent:v2026.6.19 .

# 验证镜像构建成功
docker images | grep hermes-agent
# 期望输出:hermes-agent   v2026.6.19   <image_id>   <size>

构建参数说明:

参数 说明
-t hermes-agent:v2026.6.19 镜像名称和标签,后续 docker run / compose.yml 中引用的就是这个名字
. 构建上下文为当前目录(包含 Dockerfile)

自定义 rtk 版本(可选):

docker build --build-arg RTK_VERSION=0.42.4 -t hermes-agent:v2026.6.19 .

国内构建加速提示:Dockerfile 已切换国内镜像源(apt/npm/PyPI),无需额外配置代理。如果 docker build 本身拉取基础镜像慢,可配置 Docker daemon 的 registry-mirrors

部署步骤

1. 启动容器

方式一:docker run

docker run -d \
    --name hermes \
    --restart unless-stopped \
    -p 127.0.0.1:9119:9119 \
    -v ~/.hermes:/opt/data \
    -e TZ=Asia/Shanghai \
    -e HERMES_DASHBOARD=true \
    -e HERMES_DASHBOARD_INSECURE=true \
    hermes-agent:v2026.6.19 gateway run

方式二:docker compose(推荐)

compose.yml

services:
  hermes:
    image: hermes-agent:v2026.6.19
    container_name: hermes
    restart: unless-stopped
    ports:
      - "127.0.0.1:9119:9119"
    volumes:
      - ~/.hermes:/opt/data
    environment:
      - HERMES_UID=${HERMES_UID:-10000}
      - HERMES_GID=${HERMES_GID:-10000}
      - TZ=Asia/Shanghai
      - HERMES_DASHBOARD=true
      - HERMES_DASHBOARD_INSECURE=true
    command: ["gateway", "run"]

启动:

docker compose up -d

9119 端口绑定 127.0.0.1,仅本机访问。如需远程访问,通过 SSH 隧道或反向代理转发,不要直接绑定 0.0.0.0

2. 验证运行状态

# 查看启动日志
docker logs hermes -f

# 查看 stage2-hook 初始化输出
docker logs hermes 2>&1 | grep '\[stage2\]'

# 进入容器调试(自动以 hermes 用户执行)
docker exec hermes hermes status

踩坑记录

坑 1:不要用 --user 启动容器

现象docker run --user $(id -u):$(id -g) 启动后容器直接报错退出。

原因:s6-overlay 的 cont-init.d 阶段需要 root 权限执行 UID remap、chown 等操作。非 root 启动时这些步骤全部跳过,hermes 代码树(UID 10000)对任意其他 UID 均不可写,必然 EACCES 崩溃。

正确做法:以 root 启动(默认),通过 HERMES_UID / PUID 环境变量传入宿主机 UID。

坑 2:工具重启后消失(npm/pip 包丢失)

现象:在 Hermes 内安装了飞书 CLI、某个 npm 工具,重启容器后全部找不到。

原因:若未正确配置 $HOME,npm prefix 默认落在 ~/.local(容器内非持久路径),重启后镜像层重置,安装记录清空。

本项目的解法HERMES_HOME=/opt/data 已将 $HOME 指向持久化 volume,~/.local 解析为 /opt/data/.local,PATH 也已包含 /opt/data/.local/bin。只要工具安装到 $HOME 路径下,重启后自动恢复。

坑 3:docker exec 写出 root 属主文件

现象docker exec <container> hermes ... 执行后,/opt/data 下出现 root 属主文件,下次启动 gateway 报 PermissionError。

解法一(推荐):镜像内置了 /opt/hermes/bin/hermes shim,它检测到 root 调用时自动 s6-setuidgid hermes 降权,直接 docker exec hermes hermes ... 即可安全使用。

解法二:显式指定用户 docker exec -u hermes hermes ...

坑 4:Docker 镜像拉取被拦截,可以切换国内镜像

现象docker pull 超时或报 connection refused

解法:Dockerfile 已切换到国内镜像源:

  • Debian apt 源 → mirrors.ustc.edu.cn
  • npm registry → registry.npmmirror.com
  • PyPI → pypi.tuna.tsinghua.edu.cn
  • Node 基础镜像 → docker.m.daocloud.io/library/node:22-bookworm-slim

坑 5:UID 重映射后 .venv / ui-tui 权限错误

现象:指定 HERMES_UID 后,lazy install(discord.py、telegram 等适配器)失败,TUI 每次重新编译 dist/entry.js

原因usermod -u <new> hermes 会重新 chown $HOME/opt/data),但不会动 /opt/hermes/.venv/opt/hermes/ui-tui 等构建产物,它们仍属主 10000,新 UID 无法写入。

解法:stage2-hook.sh 在每次启动时检测 venv 属主,若与运行时 UID 不一致则执行一次 chown -R

venv_owner=$(stat -c %u "$INSTALL_DIR/.venv")
if [ "$venv_owner" != "$actual_hermes_uid" ]; then
    chown -R hermes:hermes "$INSTALL_DIR/.venv" "$INSTALL_DIR/ui-tui" ...
fi

安全注意事项

  1. 不要暴露 6789/6790 端口到公网:Web UI 和 Gateway 默认无认证,应通过反向代理(nginx/caddy)加 HTTPS + BasicAuth 后再对外。

  2. API Key 保存在 /opt/data/.env:文件权限为 600,仅 hermes 用户可读,不要将其纳入 git。

  3. 镜像完整性校验:s6-overlay tarball 在构建时通过 SHA256 校验,防止供应链攻击。

总结

Docker 部署 Hermes Agent 的核心要点:

  • 目录分离:代码层只读,数据层持久化
  • 权限管理:s6-overlay + hermes 用户降权
  • 国内优化:镜像源全面切换到国内镜像
  • 工具持久化:HERMES_HOME=/opt/data 确保 npm/pip 工具不丢失

按照本文档操作,应该能够顺利部署并运行 Hermes Agent。如有问题,欢迎在评论区交流。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐