solutionhacker
Case SH-003Agents

How to Set Up Hermes Agent on a VPS — No Docker, Just Root

works

Updated  · 9 min read

Problem Running Hermes in Docker meant every .env change triggered a container redeploy that replaced the config file.

Solution

Run Hermes directly as root on a bare Ubuntu VPS, with the gateway as a systemd service, Telegram, Cloudflare Tunnel and Claude Code. Your .env at /root/.hermes/.env is then a plain file on disk that nothing overwrites.

A step-by-step walkthrough for running Hermes Agent directly on a bare VPS with systemd, Telegram, Cloudflare Tunnel, and Claude Code. No containers, no orchestration — just a clean Linux box doing one job well.

Introduction

Hermes Agent is an open-source AI agent built by Nous Research. It runs in your terminal and on messaging platforms like Telegram, Discord, and Slack. You give it a task — write code, analyze data, build an HTML page — and it uses tools to get it done. Think of it as a persistent, multi-platform assistant that actually executes instead of just chatting.

Most guides assume Docker. I started with Docker too and ran into a specific problem: changing .env variables triggers a container redeploy, which replaces your config file. Running Hermes directly as root avoids this — your .env at /root/.hermes/.env is a plain file on disk that nothing overwrites. No bridge networking, no volume mount puzzles, no dual-environment confusion. You SSH in, install it, configure it, and it’s yours.

By the end of this guide you’ll have:

  • Hermes gateway running 24/7 as a systemd service, connected to Telegram
  • Claude Code CLI installed for delegated coding tasks
  • Cloudflare Tunnel exposing a public HTTPS domain for web projects
  • All data living under /root/.hermes/ — one location, no confusion

Prerequisites

Before you start, make sure you have:

  • A VPS running Ubuntu 24.04 — any provider works (Hostinger, DigitalOcean, Hetzner, etc.)
  • Root SSH access to that VPS
  • A domain name managed on Cloudflare (for the tunnel)
  • API keys for at least one LLM provider — Z.AI/GLM, OpenRouter, OpenAI, Anthropic, etc.
  • A Telegram bot token from @BotFather

That’s it. Let’s go.

Install Hermes

One command installs everything — the CLI, the virtual environment, and the systemd helpers:

curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash

Verify it worked:

hermes --version

The install lands at /usr/local/lib/hermes-agent/ with a Python venv inside. The hermes binary is symlinked into /usr/local/bin/.

Configure Your Model Provider

The easiest way is the interactive wizard:

hermes model

Or configure manually if you know exactly what you want. Here’s an example with Z.AI’s GLM model:

hermes config set model.default glm-5.1
hermes config set model.provider zai
hermes config set model.base_url https://api.z.ai/api/coding/paas/v4

Add your API key to the environment file:

echo "GLM_API_KEY=your_key_here" >> ~/.hermes/.env
echo "GLM_BASE_URL=https://api.z.ai/api/coding/paas/v4" >> ~/.hermes/.env

Tip. The .env file lives at /root/.hermes/.env. Since you’re running directly on the host (not Docker), nothing overwrites it. It persists across restarts and updates. Just don’t commit it to git.

Set Up Telegram Gateway

First, create a bot via @BotFather on Telegram. Send /newbot, pick a name, and copy the token it gives you.

Add the Telegram config to your .env:

# Add Telegram config to .env
echo "TELEGRAM_BOT_TOKEN=your_bot_token" >> ~/.hermes/.env
echo "TELEGRAM_HOME_CHANNEL=your_telegram_user_id" >> ~/.hermes/.env

# Authorize additional Telegram users (comma-separated IDs)
echo "TELEGRAM_ALLOWED_USERS=your_id,family_member_id" >> ~/.hermes/.env

Now install the Telegram adapter into Hermes’ venv:

/usr/local/lib/hermes-agent/venv/bin/pip install python-telegram-bot

Important. After every hermes update, reinstall this package. Updates rebuild the venv and drop optional dependencies. This is the single most common “why did Telegram stop working” cause.

Understanding Hermes File Structure

Hermes keeps everything under /root/.hermes/. Here’s what lives where:

/root/.hermes/
├── .env                    # API keys, tokens, secrets (never overwritten on bare metal)
├── config.yaml             # Model config, tool settings, feature flags
├── memories/
│   ├── MEMORY.md           # Agent's persistent notes (environment facts, lessons learned)
│   └── USER.md             # User profile and preferences
├── sessions/               # Conversation transcripts (JSON, one per chat)
├── skills/                 # Reusable workflows and procedures (markdown + scripts)
├── pairing/                # Telegram user authorization (alternative to TELEGRAM_ALLOWED_USERS)
├── projects/               # Static files served by your HTTP server
├── logs/
│   └── gateway.log         # Telegram gateway output
└── scripts/                # Custom Python scripts

The key takeaway: .env, memories/, sessions/, and skills/ are your data. Back up /root/.hermes/ and you back up everything.

Why not Docker? When Hermes runs in Docker, changing .env triggers a container redeploy that replaces the file. On bare metal, .env is just a file on disk — you edit it, it stays edited. That’s the entire motivation for the root approach.

Install Gateway as Systemd Service

Create /etc/systemd/system/hermes-gateway.service:

[Unit]
Description=Hermes Agent Gateway
After=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/lib/hermes-agent/venv/bin/python -m hermes_cli.main gateway run --replace
WorkingDirectory=/usr/local/lib/hermes-agent
Environment="HOME=/root"
Environment="PATH=/usr/local/lib/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin"
Environment="HERMES_HOME=/root/.hermes"
Restart=always
RestartSec=60

[Install]
WantedBy=multi-user.target

Enable and start:

systemctl daemon-reload
systemctl enable --now hermes-gateway
systemctl status hermes-gateway

Check logs to confirm it’s running:

tail -f ~/.hermes/logs/gateway.log

You should see a Connected message from Telegram within a few seconds. The service auto-restarts on crash and boots with the system.

Set Up Cloudflare Tunnel

Install cloudflared:

curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
dpkg -i cloudflared.deb

Login and create a tunnel:

cloudflared tunnel login
cloudflared tunnel create hermes-demo

The tunnel login command will give you a URL — open it in a browser, pick your domain, and authorize. The tunnel create command outputs a UUID — save it.

Edit /etc/cloudflared/config.yml:

tunnel: YOUR_TUNNEL_UUID
credentials-file: /root/.cloudflared/YOUR_TUNNEL_UUID.json

ingress:
  - hostname: demo.yourdomain.com
    service: http://127.0.0.1:8000
  - service: http_status:404

Add the DNS route (this creates a CNAME on Cloudflare automatically):

cloudflared tunnel route dns hermes-demo demo.yourdomain.com

Install as a systemd service:

cloudflared service install
systemctl enable --now cloudflared

Now https://demo.yourdomain.com/ is live, tunneling to localhost:8000 on your VPS. Permanent URL, real TLS cert, survives reboots.

Serve Web Projects

Create a systemd service for static file serving. Create /etc/systemd/system/projects-http.service:

[Unit]
Description=Projects HTTP Server (port 8000)
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 -m http.server 8000 --directory /root/.hermes/projects
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable it:

systemctl daemon-reload
systemctl enable --now projects-http

Now write HTML to any subfolder and it’s instantly live:

mkdir -p /root/.hermes/projects/my-app
echo 'Hello from Hermes' > /root/.hermes/projects/my-app/index.html

Visit https://demo.yourdomain.com/my-app/ — it loads. Your agent can now ship HTML artifacts to the world by writing files to /root/.hermes/projects/.

Services Overview

At this point you have three systemd services running. Here’s the full picture:

Service What it does Port / Endpoint
hermes-gateway Hermes Telegram gateway (reads .env, connects to Telegram, processes messages) N/A (outbound to Telegram API)
projects-http Serves static files from /root/.hermes/projects/ 127.0.0.1:8000
cloudflared Cloudflare Tunnel — proxies public HTTPS to localhost:8000 https://demo.yourdomain.com → localhost:8000

All three auto-start on boot and restart on crash. Check them anytime:

systemctl is-active hermes-gateway projects-http cloudflared

The data flow: Telegram message → Hermes gateway (processes with LLM) → writes HTML to projects/ → served by HTTP server → tunnel makes it public.

Install Claude Code CLI

Claude Code is Anthropic’s agentic coding tool. It’s a nice complement to Hermes — use it for deep coding sessions while Hermes handles the conversational and multi-platform side.

npm install -g @anthropic-ai/claude-code

Authenticate with your Anthropic subscription:

claude login

This stores credentials at ~/.claude/.credentials.json. Verify it works:

claude -p "Say hello" --max-turns 1

You should get a response back. If you do, Claude Code is ready to use from the terminal or to be called by Hermes as a tool.

Final Verification

Run this checklist to confirm everything is wired:

# All services running?
systemctl is-active hermes-gateway projects-http cloudflared

# Telegram connected?
grep "Connected" ~/.hermes/logs/gateway.log | tail -1

# Site live?
curl -s -o /dev/null -w "%{http_code}" https://demo.yourdomain.com/

# Claude Code working?
claude -p "hello" --max-turns 1

Expected results: three active statuses, a Connected log line, an HTTP 200, and a text response from Claude. If all four pass, you’re done.

Pitfalls We Learned the Hard Way

  • Hermes updates wipe optional deps. Always reinstall python-telegram-bot after hermes update. The update rebuilds the venv from scratch and only installs core requirements.
  • .env is safe on bare metal. The “env gets wiped” problem only happens in Docker (container redeploy replaces the file). On a root install, /root/.hermes/.env is a plain file — edit it, it stays edited. Just add your TELEGRAM_ALLOWED_USERS directly in .env.
  • Check API keys with anchored grep. Use grep "^KEY=" ~/.hermes/.env — not just grep KEY, which matches commented-out lines and leads you down rabbit holes.
  • Reasoning models are slow for chat. glm-5.1 is a reasoning model — great for complex tasks, but it thinks for 30+ seconds before replying. Use reasoning_effort: low or switch to a non-reasoning model for snappy Telegram conversations.
  • Systemd + file edits in one command. Don’t pipe a heredoc into a service file and immediately run systemctl daemon-reload in the same command. Split into two: write the file first, then reload and restart.

What You Have Now

When you’re done, here’s the full picture:

  • Hermes Agent running 24/7 as a systemd service — survives reboots, auto-restarts on crash
  • Telegram bot connected — chat with your agent from your phone, anywhere
  • Cloudflare Tunnel — host HTML projects with HTTPS on your own domain, no port forwarding, no cert management
  • Claude Code CLI — delegate coding tasks to Claude from the same VPS
  • All data at /root/.hermes/ — one location, no confusion, easy to back up

No Docker. No compose files. No bridge networks. Just a clean Linux box, doing one job well, and an agent that’s always there when you message it.


This setup evolved from a weekend of trial-and-error documented in this field notes post. The lessons there — pre-wire infrastructure, fix the source not the symptom, reduce the agent’s decision surface — are what led to the clean systemd-native approach described here.

Questions people ask

Why did Telegram stop working after hermes update?
Updates rebuild the venv from scratch and only install core requirements, which drops python-telegram-bot. Reinstall it after every hermes update.
Why is my Telegram bot so slow to reply?
glm-5.1 is a reasoning model and thinks for 30+ seconds before replying. Use reasoning_effort: low or switch to a non-reasoning model for snappy chat.
How should I check which API keys are set?
Use an anchored grep like grep "^KEY=" ~/.hermes/.env. A plain grep KEY also matches commented-out lines and leads you down rabbit holes.