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 sshfrom any device on your tailnet. For end users, it shows up in Slack — outbound-initiated Socket Mode, no inbound ports.
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.mdfor what tokens you'll need) - Tailscale installed and running on any device you want to maintain the agent from
Clicking this button will:
- Ask you to connect your GitHub account if you haven't already
- Clone this repo into your specified GitHub account or organization
- Prompt you to fill in the required environment variables (see below)
- 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.
These are set in Railway during and after deployment. They are never stored in this repository.
| 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. |
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.)
| 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.
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.
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.
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 logsTip: Always SSH as
hermes@...rather thanroot@.... 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).
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:9119Full walkthrough (including Windows OpenSSH, MagicDNS fallbacks, and multi-agent port conventions): docs/dashboard-access.md.
Hermes is pinned to a specific version in the Dockerfile. It will not auto-update.
To upgrade:
- Check the Hermes releases page and review the changelog for breaking changes
- Update the
HERMES_VERSIONvalue in your repo'sDockerfile - Push the change — Railway will automatically redeploy
- 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.
There are two supported patterns, depending on how independent the agents are:
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.
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.
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/mainReview any changes before merging — particularly changes to the Dockerfile (version bumps) and entrypoint.sh.
| 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 |
The template is designed to make accidental exposure structurally difficult, but here are the things to watch:
- Never generate a public Railway domain for the service. The Dockerfile has no
EXPOSEdirective, 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. - Never commit API keys or bot tokens. All secrets live in Railway environment variables. The
.gitignorein this repo blocks common secret file patterns, but always double-check before pushing. - 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.
- Review the Tailscale ACL before adding team members. Your Tailscale ACL controls who on your tailnet can
tailscale sshwhere — and, if the control sidecar is enabled, who can reach it. Ensure only authorized engineers and services can reach agent nodes. - Don't set
GATEWAY_ALLOW_ALL_USERS=true. This Hermes env var disables all per-platform allowlists and turns the bot into open-access. Seedocs/secrets-guide.mdandHANDOFF.mdRisk 7. - Keep
ROSTER_CONTROL_TOKENper-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.
Deploy completes but the agent doesn't appear in Tailscale admin
- Check the Railway service logs — is
TS_AUTHKEYset? If the entrypoint printsTS_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_USERSis missing or doesn't contain your Slack member ID. SeeHANDOFF.mdRisk 7.
hermes command not found from an SSH session
- SSH as
hermes@<hostname>, notroot@<hostname>— though the symlink at/usr/local/bin/hermesshould 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_HOMEandHOMEenv vars — they should NOT be set in Railway. If either is set, remove them and redeploy.
See ARCHITECTURE.md for a detailed breakdown of how the components connect, where data lives, and the reasoning behind key decisions.
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.