Skip to content

Repository files navigation

rome-on-rails

A Railway deployment template for Hermes — the self-improving AI agent by Nous Research — built for Echobind's internal use. Private by default. Tailscale-gated. One-click deploy.

Security model in one sentence: Each Hermes deployment is itself a Tailscale node with SSH enabled but no public URL. For maintenance, reach it via tailscale ssh from any device on your tailnet. For end users, it shows up in Slack — outbound-initiated Socket Mode, no inbound ports.


Prerequisites

Before you deploy, you need:

  • A Railway account
  • A Tailscale account with at least one device connected (your laptop or workstation)
  • A reusable Tailscale auth key (from login.tailscale.com/admin/settings/keys)
  • An API key for your chosen LLM provider (OpenRouter is the easiest starting point)
  • A Slack app installed to your workspace (see docs/secrets-guide.md for what tokens you'll need)
  • Tailscale installed and running on any device you want to maintain the agent from

Deploy

Deploy on Railway

Clicking this button will:

  1. Ask you to connect your GitHub account if you haven't already
  2. Clone this repo into your specified GitHub account or organization
  3. Prompt you to fill in the required environment variables (see below)
  4. Deploy a single Hermes service into a new Railway project

⚠️ Do not generate a public domain for the service. The template deploys Hermes as a worker with no public URL. Hermes doesn't listen on any inbound port; adding a domain today is a no-op, but preserving that property protects you against future Dockerfile changes accidentally exposing surface.


Required Environment Variables

These are set in Railway during and after deployment. They are never stored in this repository.

Tailscale (required for maintainer access)

Variable Required Description
TS_AUTHKEY Yes Reusable Tailscale auth key from tailscale.com/admin/settings/keys. Without this, the container can't register with your tailnet and you won't be able to SSH in. Slack will still work.
TS_HOSTNAME No (see note) Custom tailnet hostname. Defaults to ${RAILWAY_PROJECT_NAME}-${RAILWAY_ENVIRONMENT_NAME}. Required if you duplicate this service to run multiple agents in the same Railway project — the default is identical for every service in a project and collides in Tailscale. See docs/multi-agent.md.
TS_EXTRA_ARGS No Extra flags passed verbatim to tailscale up (e.g., --advertise-tags=tag:hermes-agent if you use ACL tags). Do not include --authkey, --hostname, or --ssh — the entrypoint sets those.

LLM provider (at least one required)

Set the API key for whichever provider you want Hermes to use. OpenRouter is recommended for a first deploy — one key gives access to 200+ models.

Variable Required Description
OPENROUTER_API_KEY If using OpenRouter Key from openrouter.ai/keys
ANTHROPIC_API_KEY If using Anthropic directly Key from the Anthropic Console
OPENAI_API_KEY If using OpenAI directly Key from the OpenAI dashboard
GOOGLE_API_KEY If using Google Gemini directly Key from Google AI Studio
HERMES_INFERENCE_PROVIDER Optional Explicitly selects the provider (openrouter, anthropic, openai, google). Defaults to auto.

The specific model to use is set from inside the container after first boot, via tailscale ssh hermes@<hostname>hermes model (or hermes config edit). Hermes writes your choice to /opt/data/config.yaml on the persistent volume. (If you've enabled the Roster control sidecar — see below — Roster can also read and change the model remotely; it writes the same file the same way.)

Slack gateway (required for Slack)

Variable Required Description
SLACK_BOT_TOKEN Yes, for Slack Bot token, format xoxb-...
SLACK_APP_TOKEN Yes, for Slack App-level token for Socket Mode, format xapp-...
SLACK_ALLOWED_USERS Yes, for Slack to function Comma-separated Slack member IDs, e.g., U01234ABCDE,U05678FGHIJ. If unset, the Hermes gateway denies all messages and the bot appears online but silent. See HANDOFF.md Risk 7.

If Slack tokens are absent, the container still starts (Slack just doesn't connect) and the entrypoint logs a warning. If SLACK_ALLOWED_USERS is absent, Hermes itself emits a warning and rejects all messages — the bot appears online but unresponsive.

Roster control sidecar (optional)

These enable the Roster control sidecar — a tailnet-only HTTP service that lets Echobind's agent control plane (Roster) read and change this agent's LLM model remotely. If you're deploying by hand, you can ignore both: leave ROSTER_CONTROL_TOKEN unset and the sidecar simply doesn't start. When Roster provisions an agent, it sets these automatically.

Variable Required Description
ROSTER_CONTROL_TOKEN No Bearer token for the control sidecar. Unset ⇒ the sidecar does not start and the agent is not remotely controllable (Slack and tailscale ssh are unaffected). Set per-agent — never reuse a token across agents. Treat as a secret.
ROSTER_CONTROL_PORT No Port the sidecar binds on 127.0.0.1 and is served on over the tailnet. Defaults to 8765. Only change it if 8765 collides with something else on the agent.

The sidecar adds no public network surface — it binds 127.0.0.1 and is reached only over the tailnet (via tailscale serve), exactly like tailscale ssh. Full interface details: docs/hermes-control-sidecar-contract.md.

Other env vars you should not need to set

The upstream Hermes image already sets HERMES_HOME=/opt/data and declares the volume at that path. Do not set HERMES_HOME, HOME, or GATEWAY_ALLOW_ALL_USERS — see docs/secrets-guide.md for details on why.

Additional optional variables (Telegram, Discord, WhatsApp tokens, custom base URLs, timeout overrides) are documented in docs/secrets-guide.md and in the upstream environment variables reference.


Post-Deploy: Connecting to your agent

After the deploy succeeds, the container boots, tailscaled starts, and the agent registers with your tailnet. Within a minute, you should see a new machine in your Tailscale admin console named <project-name>-<environment-name> (or whatever you set TS_HOSTNAME to).

From any device on your tailnet with Tailscale running:

tailscale ssh hermes@<agent-hostname>

You're inside the container as the hermes user. Try:

hermes status      # overall state
hermes model       # configure which LLM model to use
hermes config      # view / edit configuration
hermes skills      # list installed skills
hermes logs        # tail gateway logs

Tip: Always SSH as hermes@... rather than root@.... The hermes user's environment owns the files Hermes manages; running CLI commands as root writes files with root ownership and confuses the gateway process later.

That's it — no subnet route approval, no split DNS configuration, no port forwarding. Tailscale SSH authenticates you by your tailnet identity; no keys to manage.

See docs/tailscale-setup.md for deeper Tailscale topics (ACLs, hostname customization, troubleshooting).

Reaching the Hermes web dashboard (optional, ad-hoc)

The dashboard is not started by default. When you need it, open an SSH session with a local-port forward and run hermes dashboard inside:

ssh -L 9119:localhost:9119 hermes@<agent-hostname>
# inside the container:
hermes dashboard &
# on your laptop:
open http://localhost:9119

Full walkthrough (including Windows OpenSSH, MagicDNS fallbacks, and multi-agent port conventions): docs/dashboard-access.md.


Updating Hermes

Hermes is pinned to a specific version in the Dockerfile. It will not auto-update.

To upgrade:

  1. Check the Hermes releases page and review the changelog for breaking changes
  2. Update the HERMES_VERSION value in your repo's Dockerfile
  3. Push the change — Railway will automatically redeploy
  4. After redeploy, SSH in and verify — particularly that Slack still behaves correctly under the same SLACK_ALLOWED_USERS (Hermes has been known to change allowlist behavior between versions)

Do not run hermes update inside the container. That command upgrades Hermes unconditionally and bypasses the pinned version.


Multi-Agent: running several Hermes instances

There are two supported patterns, depending on how independent the agents are:

Pattern A — separate Railway projects (one per client or team)

Deploy the template a second time (click the button again). You get a fresh Railway project with its own Hermes service. Each project:

  • Has its own Tailscale auth key
  • Registers as a separate tailnet node with a unique hostname
  • Has its own Slack app, LLM provider key, and Railway access list

Use this when agents belong to different clients, different billing owners, or different maintainer groups. It's the simplest and cleanest isolation.

Pattern B — multiple services in one Railway project (one per agent within a client's fleet)

Deploy the template once, then use Railway's Duplicate Service action to create additional Hermes services inside the same project. Each service registers as its own tailnet node with its own Slack app.

Use this when several agents serve the same client — one Slack workspace with multiple bots, or multiple Slack workspaces owned by the same organization. Shared across the project: the LLM provider key and TS_AUTHKEY. Per-agent: TS_HOSTNAME, SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_ALLOWED_USERS.

Full walkthrough — including the footgun where duplicated services ship with the original's Slack tokens and accidentally cause two bots to answer every message — in docs/multi-agent.md.


Keeping Up With Template Changes

Because you deployed this template into your own repo, your copy is independent of echobind/rome-on-rails. To pull in future improvements:

git remote add upstream https://github.com/echobind/rome-on-rails.git
git fetch upstream
git merge upstream/main

Review any changes before merging — particularly changes to the Dockerfile (version bumps) and entrypoint.sh.


What Is Public vs. Private

Item Visibility Notes
This repository Public Required for Railway deploy button
Dockerfile Public No secrets — only the pinned Hermes version
entrypoint.sh Public Reads secrets from env at runtime
roster-control-sidecar.py Public No secrets — reads its bearer token from env at runtime
Railway environment variables Private Set in Railway dashboard only
The Hermes container itself Private No public URL; only reachable via tailscale ssh, outbound Slack WebSocket, or — if enabled — the control sidecar over the tailnet
Roster control sidecar endpoint Private Binds 127.0.0.1; reachable only over the tailnet via tailscale serve, gated by a bearer token
Volume contents Private Stored in Railway infrastructure

How to Avoid Accidentally Exposing Hermes

The template is designed to make accidental exposure structurally difficult, but here are the things to watch:

  1. Never generate a public Railway domain for the service. The Dockerfile has no EXPOSE directive, so generating a domain today is a no-op. Don't rely on that — adding an HTTP server in the future would change the equation.
  2. Never commit API keys or bot tokens. All secrets live in Railway environment variables. The .gitignore in this repo blocks common secret file patterns, but always double-check before pushing.
  3. Don't enable Tailscale Funnel on your agent's tailnet node. Funnel is a Tailscale feature that exposes a tailnet service to the public internet. This template deploys without Funnel and you should keep it that way. This matters more if you've enabled the Roster control sidecar — that is a real HTTP service, and the tailnet (plus its bearer token) is the only thing keeping it private. Funnel would undo that.
  4. Review the Tailscale ACL before adding team members. Your Tailscale ACL controls who on your tailnet can tailscale ssh where — and, if the control sidecar is enabled, who can reach it. Ensure only authorized engineers and services can reach agent nodes.
  5. Don't set GATEWAY_ALLOW_ALL_USERS=true. This Hermes env var disables all per-platform allowlists and turns the bot into open-access. See docs/secrets-guide.md and HANDOFF.md Risk 7.
  6. Keep ROSTER_CONTROL_TOKEN per-agent and secret. If you use the Roster control sidecar, its bearer token is the authorization layer for remotely changing the agent's model. Never reuse one token across agents, never commit it, and rotate it if it leaks. Unset it entirely and the sidecar won't even start.

Troubleshooting

Deploy completes but the agent doesn't appear in Tailscale admin

  • Check the Railway service logs — is TS_AUTHKEY set? If the entrypoint prints TS_AUTHKEY is not set, you forgot to add it
  • Check the tailscaled logs in Railway output for auth errors (expired key, revoked key, non-reusable key already consumed)

tailscale ssh hermes@<hostname> says "connection refused" or "no such host"

  • Confirm Tailscale is running on your laptop (tailscale status)
  • Confirm the machine shows up in the admin console
  • Check that your Tailscale ACL allows you to reach it
  • Try the fully-qualified MagicDNS name: <hostname>.<tailnet-name>.ts.net

Bot is online in Slack but doesn't respond to messages

  • Most likely cause: SLACK_ALLOWED_USERS is missing or doesn't contain your Slack member ID. See HANDOFF.md Risk 7.

hermes command not found from an SSH session

  • SSH as hermes@<hostname>, not root@<hostname> — though the symlink at /usr/local/bin/hermes should make it work for either. If you SSHed as hermes and still see this, the Dockerfile's symlink step failed during build — check the Railway build logs.

Service crashes on startup with a volume-related error

  • Check HERMES_HOME and HOME env vars — they should NOT be set in Railway. If either is set, remove them and redeploy.

Architecture

See ARCHITECTURE.md for a detailed breakdown of how the components connect, where data lives, and the reasoning behind key decisions.


Handoff Notes

See HANDOFF.md for a summary of known risks and recommendations for future maintainers.


Built and maintained by Echobind. Hermes is developed by Nous Research under the MIT license.

About

A Railway template to deploy a Hermes AI agent tool behind a Tailnet

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages