How to Set Up Hermes Agent on a VPS — No Docker, Just Root
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
.envfile 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
.envtriggers a container redeploy that replaces the file. On bare metal,.envis 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-botafterhermes update. The update rebuilds the venv from scratch and only installs core requirements. .envis safe on bare metal. The “env gets wiped” problem only happens in Docker (container redeploy replaces the file). On a root install,/root/.hermes/.envis a plain file — edit it, it stays edited. Just add yourTELEGRAM_ALLOWED_USERSdirectly in.env.- Check API keys with anchored grep. Use
grep "^KEY=" ~/.hermes/.env— not justgrep KEY, which matches commented-out lines and leads you down rabbit holes. - Reasoning models are slow for chat.
glm-5.1is a reasoning model — great for complex tasks, but it thinks for 30+ seconds before replying. Usereasoning_effort: lowor 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-reloadin 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.