diff --git a/hermes/.env.example b/hermes/.env.example index abd9b4d..a4d62a7 100644 --- a/hermes/.env.example +++ b/hermes/.env.example @@ -11,16 +11,15 @@ HERMES_DASHBOARD_PASSWORD= # Signs dashboard sessions; keep it stable. Generate with: openssl rand -hex 32 HERMES_DASHBOARD_SECRET= -# Model provider for the agent. -OPENROUTER_API_KEY= - # Telegram gateway: bot token, then comma-separated user ids and group chat ids # allowed to talk to it. An empty token leaves Telegram off. TELEGRAM_BOT_TOKEN= TELEGRAM_ALLOWED_USERS= TELEGRAM_GROUP_ALLOWED_CHATS= -# Other providers. Uncomment here and in compose.yml to enable one. +# Provider keys normally live in /opt/data/.env, set from the dashboard. +# Uncomment here and in compose.yml to pass one from the environment instead. +# OPENROUTER_API_KEY= # ANTHROPIC_API_KEY= # OPENAI_API_KEY= # GOOGLE_API_KEY= diff --git a/hermes/Dockerfile b/hermes/Dockerfile deleted file mode 100644 index 4e2cf24..0000000 --- a/hermes/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nousresearch/hermes-agent:latest - -# Workspace directory owned by the hermes user (HERMES_UID/HERMES_GID 1000). -RUN mkdir -p /workspace && chown 1000:1000 /workspace diff --git a/hermes/README.md b/hermes/README.md index 791ee6c..a99a636 100644 --- a/hermes/README.md +++ b/hermes/README.md @@ -4,19 +4,19 @@ agent with persistent memory, scheduling and chat-platform gateways, from Nous Research's official image, with its built-in web dashboard. -One container. `gateway run` starts the agent gateway, and the image's s6 -supervisor also starts the dashboard on port `9119`: chat (the Hermes -terminal UI in the browser), sessions, config, cron, skills and logs. +Laid out after upstream's +[Docker guide](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/docker.md): +one container, one data volume at `/opt/data`, `gateway run` as the command, +and `HERMES_DASHBOARD=1` so the image's s6 supervisor also runs the dashboard +on port `9119`. ## Setup -1. Set `HERMES_DASHBOARD_PUBLIC_URL`, `HERMES_DASHBOARD_PASSWORD`, - `HERMES_DASHBOARD_SECRET` and `OPENROUTER_API_KEY`. +1. Set `HERMES_DASHBOARD_PUBLIC_URL`, `HERMES_DASHBOARD_PASSWORD` and + `HERMES_DASHBOARD_SECRET`. 2. Map the domain to port `9119` and deploy. -3. Open the domain and log in with `HERMES_DASHBOARD_USERNAME` / - `HERMES_DASHBOARD_PASSWORD`. - -Health check: `GET /api/status`, also the compose healthcheck. +3. Open the domain, log in with `HERMES_DASHBOARD_USERNAME` / + `HERMES_DASHBOARD_PASSWORD`, and add a model provider under Config. ## Environment @@ -25,50 +25,43 @@ Health check: `GET /api/status`, also the compose healthcheck. | `HERMES_DASHBOARD_PUBLIC_URL` | — | Full public URL, e.g. `https://hermes.example.com` | | `HERMES_DASHBOARD_USERNAME` / `HERMES_DASHBOARD_PASSWORD` | `hermes` / — | Dashboard login | | `HERMES_DASHBOARD_SECRET` | — | Signs dashboard sessions | -| `OPENROUTER_API_KEY` | empty | Model provider | | `TELEGRAM_BOT_TOKEN` | empty | Telegram bot; empty leaves Telegram off | | `TELEGRAM_ALLOWED_USERS` / `TELEGRAM_GROUP_ALLOWED_CHATS` | empty | Telegram users and group chats allowed to use the bot | -| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY` | optional | Other model providers | +| `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY` | optional | Provider keys passed from the environment | -`HERMES_DASHBOARD=1` turns the dashboard on. Bound to `0.0.0.0`, it refuses to -start without an auth provider, so the password is required. The image's -username/password provider is the one that needs no outside identity service; -upstream describes it as meant for trusted networks and recommends OAuth (Nous -Portal) or self-hosted OIDC for a public domain. +The dashboard refuses to start on a non-loopback bind without an auth +provider, so the password is required. The username/password provider is the +one that needs no outside identity service; upstream describes it as meant +for trusted networks and recommends OAuth (Nous Portal) or self-hosted OIDC +for a public domain. `HERMES_DASHBOARD_PUBLIC_URL` adds the domain to the dashboard's Host and WebSocket Origin guard, which rejects requests for any other host. - `HERMES_DASHBOARD_SECRET` keeps sessions valid across restarts; without it -each restart signs with a new random key and logs everyone out. +each restart signs with a new random key. -The uid/gid are pinned to `1000` in `compose.yml`; the `Dockerfile` depends on -that (see Storage). +Provider keys are commented out because upstream keeps them in +`/opt/data/.env`, written from the dashboard, so they survive without being +repeated in Coolify. An environment variable, when set, wins over that file. ## Storage | Volume | Mount | Holds | | --- | --- | --- | -| `hermes-data` | `/opt/data` | `HERMES_HOME`: config, `.env`, sessions, memory, skills, logs | -| `hermes-workspace` | `/workspace` | Files the agent works on | +| `hermes-data` | `/opt/data` | `HERMES_HOME`: config, `.env`, sessions, memory, skills, logs, and the agent's working files | -The image hard-blocks the agent's file tools from writing outside -`HERMES_WRITE_SAFE_ROOT`, which it sets to `/opt/data` alone, so -`compose.yml` adds `/workspace` to it. `TERMINAL_CWD` starts gateway and cron -terminal sessions in `/workspace`. Upstream marks that variable deprecated in -favour of `terminal.cwd` in `config.yaml`; it is used here so the setting stays -in the compose file rather than on the volume. +`/opt/data` is the image's own home for all mutable state: it is the `hermes` +user's home directory, the image declares it a volume, and the dashboard's +start script uses it directly. The agent's file tools may only write under +it. The `hermes` user keeps the image's default uid, `10000`, and the image fixes +the volume's ownership for that user at start. -The `Dockerfile` exists only to make `/workspace` writable. The image does not -ship that directory and its init chowns only `/opt/data`, so a named volume on -`/workspace` would come up `root:root`. Creating the directory in the image, -owned by `1000:1000`, fixes that: Docker seeds an empty named volume from the -image directory, ownership included. +## Resources + +The 4 GB memory and 2 CPU limits are upstream's own compose example. ## Image `nousresearch/hermes-agent:latest` moves only on stable releases, roughly weekly; upstream publishes no major tag. The agent's code lives in the image, -not a volume, so a new release takes effect on the next pull and recreate. The -first start after an upgrade migrates `config.yaml`, keeping a timestamped -backup. +not a volume, so a new release takes effect on the next pull and recreate. diff --git a/hermes/compose.yml b/hermes/compose.yml index 8d86fa8..927bc62 100644 --- a/hermes/compose.yml +++ b/hermes/compose.yml @@ -1,6 +1,6 @@ services: hermes: - build: . + image: nousresearch/hermes-agent:latest restart: unless-stopped command: gateway run environment: @@ -9,27 +9,20 @@ services: - HERMES_DASHBOARD_BASIC_AUTH_USERNAME=${HERMES_DASHBOARD_USERNAME:-hermes} - HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=${HERMES_DASHBOARD_PASSWORD:?required} - HERMES_DASHBOARD_BASIC_AUTH_SECRET=${HERMES_DASHBOARD_SECRET:?required} - - HERMES_UID=1000 - - HERMES_GID=1000 - - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} - - HERMES_WRITE_SAFE_ROOT=/opt/data:/workspace - - TERMINAL_CWD=/workspace - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_ALLOWED_USERS=${TELEGRAM_ALLOWED_USERS:-} - TELEGRAM_GROUP_ALLOWED_CHATS=${TELEGRAM_GROUP_ALLOWED_CHATS:-} + # - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} # - OPENAI_API_KEY=${OPENAI_API_KEY:-} # - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} volumes: - hermes-data:/opt/data - - hermes-workspace:/workspace - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9119/api/status"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 60s + deploy: + resources: + limits: + memory: 4G + cpus: "2.0" volumes: hermes-data: - hermes-workspace: diff --git a/openclaw/README.md b/openclaw/README.md index 00fc299..5581057 100644 --- a/openclaw/README.md +++ b/openclaw/README.md @@ -4,7 +4,12 @@ Control UI, chat-channel bots and browser automation. Runs the project's official image, in its `-browser` variant with Chromium built in. -One container, serving the gateway and the Control UI on port `18789`. +Laid out after upstream's [Docker guide](https://docs.openclaw.ai/install/docker) +and its `docker-compose.yml`: one gateway container with the same command, +path variables, three volumes, healthcheck, `init`, dropped capabilities and +`host.docker.internal` mapping, serving the gateway and Control UI on port +`18789`. Upstream's `openclaw-cli` companion service is left out; the CLI runs +inside the gateway container with `docker exec`. ## Setup @@ -17,7 +22,8 @@ One container, serving the gateway and the Control UI on port `18789`. 4. Pick a default model in the Control UI. The image's default is an OpenAI model, which needs `OPENAI_API_KEY`. -Health check: the image's own, which calls `/healthz`. +Health check: `docker-healthcheck.js` from the image, which calls `/healthz`, +on upstream's compose timings. ## Environment @@ -47,13 +53,17 @@ answers every proxied request from an untrusted address with 403 The rest of OpenClaw's settings live in `openclaw.json` on the state volume and are edited in the Control UI or with `node openclaw.mjs config set`. -The `Dockerfile` copies `openclaw.json` into the image's state directory, and -Docker seeds an empty named volume from it on first start. It holds only what -this deployment needs before anyone can log in: local gateway mode, a bind to -all interfaces, the port, and the public origin and trusted proxy range as -`${VAR}` references that OpenClaw resolves from the environment at load. The -image's own start-up `doctor --fix` keeps those references when it rewrites -the file. Without `gateway.mode` a fresh volume crash-loops. +Upstream's guide finishes setup with one-off commands run before the gateway +first starts: `onboard`, then `config set` for `gateway.mode` and +`gateway.bind`. Coolify has no step that runs a command against an app's +volume before it starts, so the `Dockerfile` does the equivalent: it copies +`openclaw.json` into the image's state directory, and Docker seeds an empty +named volume from it on first start. The file holds those two settings, the +port, and the two reverse-proxy settings from upstream's gateway reference, +`publicOrigin` and `trustedProxies`, as `${VAR}` references that OpenClaw +resolves from the environment at load. The image's start-up `doctor --fix` +keeps the references when it rewrites the file. Without `gateway.mode` a +fresh volume crash-loops. The seed applies only to a fresh volume. Editing `openclaw.json` in the repository later changes nothing for an existing deployment; change the live @@ -67,12 +77,10 @@ config instead. | `openclaw-workspace` | `/home/node/.openclaw/workspace` | Files the agent works on | | `openclaw-secrets` | `/home/node/.config/openclaw` | Auth-profile secrets | -The three mounts follow upstream's own compose. `/home/node` as a whole is not -mounted: the bundled Chromium lives in `/home/node/.cache`, and a volume there -would freeze it at the first image's version. - -`cap_drop` and `no-new-privileges` are also upstream's; the image runs as the -non-root `node` user. +The three mounts are upstream's own. `/home/node` as a whole is not mounted: +the bundled Chromium lives in `/home/node/.cache`, and a volume there would +freeze it at the first image's version. The image runs as the non-root `node` +user. ## Image diff --git a/openclaw/compose.yml b/openclaw/compose.yml index 0cdb69a..27b9cb6 100644 --- a/openclaw/compose.yml +++ b/openclaw/compose.yml @@ -2,10 +2,20 @@ services: openclaw: build: . restart: unless-stopped + init: true + command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"] environment: - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:?required} - OPENCLAW_PUBLIC_ORIGIN=${OPENCLAW_PUBLIC_ORIGIN:?required} - OPENCLAW_TRUSTED_PROXIES=${OPENCLAW_TRUSTED_PROXIES:-10.0.0.0/16} + - HOME=/home/node + - OPENCLAW_HOME=/home/node + - OPENCLAW_STATE_DIR=/home/node/.openclaw + - OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json + - OPENCLAW_CONFIG_DIR=/home/node/.openclaw + - OPENCLAW_WORKSPACE_DIR=/home/node/.openclaw/workspace + - OPENCLAW_GATEWAY_PORT=18789 + - TERM=xterm-256color - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} - TZ=${TZ:-Asia/Ho_Chi_Minh} # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} @@ -44,6 +54,14 @@ services: - NET_ADMIN security_opt: - no-new-privileges:true + extra_hosts: + - host.docker.internal:host-gateway + healthcheck: + test: ["CMD", "node", "dist/docker-healthcheck.js"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s volumes: openclaw-state: