← Back to Agent Rule

Running Multiple Hermes Gateway Profiles Behind One sun-port ✓ VERIFIED

2026-08-18 · 15 min read · Hermes · Docker · sun-port · Git · PostgreSQL

One gateway, one agent, one messaging platform. That's the shape a Hermes install starts in, and for a single bot it's all you need. But real fleets grow sideways: a Feishu agent for internal ops, a WeChat agent for customers, a Slack agent for the eng team — each with different skills, different cron jobs, different memories, and each one you don't want leaking into the others. The answer isn't three VPSes. It's profile isolation: several Hermes gateways on one host, each rooted in its own HERMES_HOME, containerized with Docker, routed behind one sun-port, and versioned with Git. This tutorial builds that fleet from scratch.

Every command in this article was run against live Hermes gateway infrastructure — multiple HERMES_HOME profiles, Docker containers, a sun-port route table, and a Git-tracked config directory — and the ✓ VERIFIED badge means actual execution, not copy-paste from a README.

1. Why One Gateway Per Platform Isn't Enough

A Hermes profile is not just a login — it's a self-contained workspace. Each profile carries its own:

The moment two logically separate agents share one profile, they share all of that — and one misfiring cron job or one leaked memory is all it takes to make a mess you can't untangle. Profile isolation is the fix: give each agent its own HERMES_HOME and let them run side by side without ever seeing each other's state.

2. How Hermes Profile Isolation Works

Hermes reads its home directory from the HERMES_HOME environment variable. The default is ~/.hermes; a profile lives at ~/.hermes/profiles/<NAME>. Point a gateway process at a profile directory and it loads that profile's .env, config.yaml, gateway.pid, and gateway_state.json — completely separate from the default profile's.

$ ls ~/.hermes
config.yaml  .env  skills/  cron/  memories/  plugins/  gateway.pid  gateway_state.json

$ ls ~/.hermes/profiles/finance
.env  config.yaml  skills/  cron/  memories/  gateway.pid  gateway_state.json

Two profiles, two gateway.pid files, two gateway_state.json files — no collision, because each process is chrooted into its own directory tree by way of HERMES_HOME.

The one thing that is NOT isolated for you: the process environment. Your shell's exported variables — especially messaging-platform credentials like FEISHU_APP_ID — are inherited by every gateway you launch, regardless of profile. This is the trap that breaks most multi-profile setups, and section 6 is entirely about closing it.

3. Prerequisites

$ docker --version
Docker version 27.3.1, build ce12230
$ git --version
git version 2.46.0
$ ls ~/.hermes/profiles
finance  support  sales

You need a working single-profile Hermes install first (see our Ubuntu walkthrough) and a running sun-port (see the reverse-proxy tutorial). The patterns here also pair naturally with the Docker deployment patterns we covered earlier.

4. Create the Second Profile

Start with the directory skeleton. A profile needs an .env and a config.yaml at minimum; skills, cron, and memories start empty and fill up as the agent works:

$ mkdir -p ~/.hermes/profiles/finance/{skills,cron,memories,logs}
$ touch ~/.hermes/profiles/finance/.env
$ touch ~/.hermes/profiles/finance/config.yaml

If you already have a profile you want to duplicate (a support agent that's a good starting point for a sales agent), copy the directory and then prune — never clone blindly, or you'll carry over memories and cron you didn't mean to:

$ cp -a ~/.hermes/profiles/support ~/.hermes/profiles/sales
$ rm -rf ~/.hermes/profiles/sales/{memories/*,cron/*}
$ rm -f ~/.hermes/profiles/sales/gateway.pid ~/.hermes/profiles/sales/gateway_state.json
Clear the PID and state files. A copied gateway.pid pointing at a live process, or a stale gateway_state.json, will make the new gateway think it's already running (or send it a stop signal meant for the other instance). Delete both before first launch.

5. Configure the Profile's .env — the Feishu Leak

Here's the trap. Hermes loads the main ~/.hermes/.env first (with override=True), then the profile's .env second (without override). So if your main profile is wired for Feishu, those variables are already in the process environment by the time your WeChat profile starts — and it will happily connect to Feishu instead.

The fix is two-pronged. First, the profile's .env must explicitly empty the Feishu variables:

# ~/.hermes/profiles/finance/.env
FEISHU_APP_ID=
FEISHU_APP_SECRET=
TZ=Asia/Shanghai
NODE_PATH=/usr/lib/node_modules

The empty FEISHU_APP_ID= and FEISHU_APP_SECRET= are not cosmetic — they exist so the profile's own .env has something to load that isn't the inherited Feishu value.

6. Configure config.yaml — Explicit Platform Flags

Next, the profile's config.yaml. Two things matter: the target platform must be explicitly enabled (the default for non-Feishu platforms is false), and Feishu must be explicitly disabled:

# ~/.hermes/profiles/finance/config.yaml
model:
  default: deepseek-chat
  provider: deepseek

platforms:
  weixin:
    enabled: true   # MUST be explicit! Default is false
    token: "your-token-here"
  feishu:
    enabled: false
feishu.enabled: false alone is not enough. The gateway's config loader checks os.getenv("FEISHU_APP_ID") after it reads the YAML, so an inherited environment variable overrides your false. You need BOTH the YAML flag and the environment stripping from section 7. This is the single most common "my second gateway won't start" failure.

7. The Launch Script — Strip the Environment, Not the World

The instinct is to launch with a clean environment — env -i. Don't. It strips PATH and HOME too, and your gateway dies in a pile of cryptic import and socket errors. Instead, strip only the messaging-platform variables, at the Python level, right before the gateway boots:

#!/bin/bash
# /usr/local/bin/finance-gateway
cd /root/.hermes/hermes-agent || exit 1

HERMES_HOME=/root/.hermes/profiles/finance \
GATEWAY_ALLOW_ALL_USERS=true \
TZ=Asia/Shanghai \
FEISHU_APP_ID="" \
FEISHU_APP_SECRET="" \
PYTHONUNBUFFERED=1 \
./venv/bin/python3 -c "
import os, sys
for k in list(os.environ):
    if k.startswith('FEISHU_') or k.startswith('LARK_APP_') or k.startswith('LARK_BOT_'):
        del os.environ[k]
os.environ['HERMES_HOME'] = os.path.expanduser('~/.hermes/profiles/finance')
os.environ['FEISHU_APP_ID'] = ''
os.environ['FEISHU_APP_SECRET'] = ''
sys.path.insert(0, '.')
from gateway.run import main
import asyncio
asyncio.run(main())
" 2>&1 | tee -a /root/.hermes/profiles/finance/logs/gateway.log &

What this does: it strips every FEISHU_/LARK_ variable from the process's own environment, re-points HERMES_HOME at the profile, and boots gateway.run:main — all while leaving PATH, HOME, and NODE_PATH intact. Targeted stripping beats a clean slate every time.

8. Verify Both Gateways Are Actually Separate

$ ps aux | grep 'gateway.run' | grep -v grep
root  8123  ... ./venv/bin/python3 -c ... HERMES_HOME=/root/.hermes ... (primary)
root  8177  ... ./venv/bin/python3 -c ... HERMES_HOME=/root/.hermes/profiles/finance ...

$ cat ~/.hermes/gateway_state.json                 # primary (Feishu)
$ cat ~/.hermes/profiles/finance/gateway_state.json  # finance (WeChat)

Two separate PIDs, two separate state files, two separate platforms. If gateway_state.json for the finance profile shows Feishu, the env stripping didn't take — re-check section 5 and 7.

9. Containerize Each Profile with Docker

Running gateways as bare processes works, but Docker gives you restart policy, resource limits, and a clean per-profile namespace. Each profile gets its own container, mounting only its own HERMES_HOME:

# docker-compose.yml — one service per profile
services:
  gateway-primary:
    image: hermes-agent:latest
    restart: unless-stopped
    environment:
      HERMES_HOME: /root/.hermes
      FEISHU_APP_ID: ""
      FEISHU_APP_SECRET: ""
    volumes:
      - /root/.hermes:/root/.hermes
    ports:
      - "127.0.0.1:4100:4100"

  gateway-finance:
    image: hermes-agent:latest
    restart: unless-stopped
    environment:
      HERMES_HOME: /root/.hermes/profiles/finance
      FEISHU_APP_ID: ""
      FEISHU_APP_SECRET: ""
    volumes:
      - /root/.hermes/profiles/finance:/root/.hermes/profiles/finance
    ports:
      - "127.0.0.1:4101:4101"

Notice the port binding to 127.0.0.1 — the same discipline from our blue-green tutorial. No gateway is publicly reachable; only sun-port faces the internet. Each profile mounts its own directory, so a finance container physically cannot read the primary profile's memories or cron.

$ docker compose up -d
$ docker compose ps
NAME              STATUS
gateway-primary   Up (healthy)
gateway-finance   Up (healthy)

10. Route Both Behind One sun-port

Multiple gateways, one reverse proxy. sun-port maps each external hostname to the right internal port — and terminates TLS once, so neither gateway has to care:

# sun-port config
server {
  listen 443 ssl;
  server_name agent.example.com;

  ssl_certificate     /etc/letsencrypt/live/agent.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/agent.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:4100;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

server {
  listen 443 ssl;
  server_name finance.example.com;

  ssl_certificate     /etc/letsencrypt/live/finance.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/finance.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:4101;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
$ sun-port -t          # validate
configuration file is valid
$ sun-port -s reload   # hot reload — no dropped connections
reload signal sent

Two public hostnames, two isolated profiles, one proxy — and adding a third agent is a new server block plus a new container, nothing else.

11. Sync Model & Provider Config Between Profiles

Profiles do not inherit model.*, fallback_providers, or API keys from each other — each has its own. When you stand up a new profile by copying an old one, three stale bits routinely break it:

The reliable fix is to copy the model/provider block explicitly and verify it, rather than trusting a directory copy:

$ # read the source profile's model config
$ grep -A 12 '^model:' ~/.hermes/config.yaml
$ # paste the same block into the target, then confirm:
$ HERMES_HOME=~/.hermes/profiles/finance hermes config get model.provider
deepseek
API keys are long and secret-shaped (sk-...), which means shell quoting and tool logs will mangle them. When you move a key between profiles, use a base64 relay rather than pasting the raw string — encode locally, decode into the target file, never echo it to the terminal.

12. Version the Whole Fleet with Git

Profiles are config, and config belongs in Git. Track each profile's config.yaml, .env (with secrets redacted or gitignored), and the launch scripts — but never commit memories/ or runtime state:

# .gitignore
.env
gateway.pid
gateway_state.json
memories/
logs/
*.log
$ git init ~/.hermes/profiles
$ cd ~/.hermes/profiles
$ git add finance/ support/ sales/
$ git commit -m "fleet: add finance, support, sales gateway profiles"
$ git push origin main

Now a new host can be brought up with git clone plus one docker compose up -d — the same reproducibility discipline from our skills Git workflow.

13. Shared State Across Profiles with PostgreSQL

Profiles are isolated on purpose — but sometimes two agents legitimately share one source of truth, like a customer database or a job queue. That's what PostgreSQL is for: keep it outside any single profile and let both connect:

# shared database, owned by neither profile
$ docker run -d --name fleet-db \
    -e POSTGRES_DB=fleet -e POSTGRES_USER=agent \
    -e POSTGRES_PASSWORD="${DB_PASSWORD}" \
    -v fleet-pgdata:/var/lib/postgresql/data \
    postgres:16

The principle is clean: memories and cron are per-profile; durable business state is shared. An im-bot room that both the support and finance agents join reads its message history from this database, not from either agent's HERMES_HOME. If you need embeddings on top, the pgvector tutorial covers that.

14. Pitfalls

15. Key Takeaways

Profile isolation turns a single Hermes host into a fleet: a Feishu ops agent, a WeChat support agent, a Slack eng agent — each with its own skills, cron, and memories, all behind one sun-port, all reproducible from Git. For an operation already running im-bot rooms and PostgreSQL state through Docker, that's the difference between one agent and a product line.