Running Hermes Agent on my Ubuntu server from macOS and Telegram

ML - DL - LLMs
How I worked through a Docker-based Hermes Agent setup on a remote Ubuntu server, connected my ChatGPT and Grok subscriptions, setup Telegram as agent interface, and gave the agent a sandboxed workspace.
Author

Bhargava Reddy Morampalli

Published

26 July 2026

Over the past year, I have seen more people running agents such as OpenClaw and Hermes Agent on their own machines, alongside growing interest in using Mac minis for automated tasks. I wanted to try one myself and chose Hermes after seeing several recommendations for it on X. Rather than buying another machine, I wanted to run it on my existing Ubuntu server. Because that server is connected to several drives containing sensitive data, I wanted to contain the agent and expose only a dedicated workspace. I also needed Hermes to keep running when my MacBook was closed and to remain accessible from both a terminal and a chat interface.

The installation itself was not the difficult part; understanding what each command had actually done was. The setup wizard ended with “Setup complete! You’re ready to go.” I therefore expected an agent to be waiting for me, but nothing happened. I first tried Discord, then encountered a missing-model error and an OpenAI login failure that looked like broken DNS even though DNS was working. This post is the guide I would have wanted at that point: a record of the setup, the mistakes and diagnostic checks along the way, and the files Hermes can and cannot see. I hope it helps anyone who wants to run Hermes in a container on a remote server, give it a deliberately limited workspace, and access it from macOS and Telegram.

Version note. I verified this workflow on 26 July 2026 with Hermes Agent 0.19.0 in Docker. Hermes is moving quickly, so model names and individual menu labels may change.

What I was building

My final arrangement looks like this:

MacBook terminal ── SSH/Tailscale ──┐
                                     ├── Hermes gateway in Docker ── model provider
Telegram bot ───── outbound polling ─┘               │
                                                     ├── /opt/data  → ~/.hermes
                                                     └── /workspace → ~/hermes-workspace

The Ubuntu server does the work. The MacBook is just one way to reach it. Telegram is another. Both entry points use the same persistent Hermes configuration and the same deliberately mounted work directory.

My starting assumptions were:

  • an Ubuntu server that I could already reach from macOS with SSH, in my case over a private Tailscale address (Tailscale is awesome!!);
  • Docker Engine installed on the Ubuntu server;
  • a ChatGPT Plus subscription for OpenAI Codex OAuth—not OpenAI API credit;
  • a Telegram account; and
  • optionally, a Grok subscription that supports xAI OAuth.

The first setup command did not start an agent

I began by making a persistent configuration directory and running the interactive setup wizard:

mkdir -p ~/.hermes

docker run -it --rm \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent setup

Two details explain what happened next:

  1. --rm means this temporary setup container is deleted when the wizard exits.
  2. -v ~/.hermes:/opt/data keeps the configuration on the Ubuntu host, so deleting that container does not delete the settings.

The wizard configured Hermes; it did not leave the gateway running. “Setup complete” meant that the files under ~/.hermes were ready for a long-running container.

Following the Hermes Docker guide, I initially launched the container with the dashboard and both documented ports:

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  -p 9119:9119 \
  -e HERMES_DASHBOARD=1 \
  nousresearch/hermes-agent gateway run

Security note. The two -p options can expose these services more widely than expected, so I used this command only as a temporary test. Docker also warns that published container ports can bypass some host firewall rules.

That worked as a starting point, but it was broader than I needed. Telegram and my SSH-based terminal access do not require these ports, so I removed the dashboard and port mappings in the final container command later in this post.

A chat channel still needs a working model

I tried to test the agent interactively:

docker exec -it hermes hermes

But there was an issue:

No inference provider configured. Run 'hermes model' to choose a provider
and model, or set an API key (OPENROUTER_API_KEY, OPENAI_API_KEY, etc.)
in ~/.hermes/.env.

This matters: setting up Telegram or Discord only creates a route to the agent. It does not supply a model that Hermes can use.

Using a ChatGPT subscription rather than an API key

I have ChatGPT Plus and SuperGrok subscriptions. Hermes supports the OpenAI Codex device-login flow for this case:

docker exec -it hermes hermes model

I selected OpenAI Codex/ChatGPT OAuth, opened the displayed device URL on my Mac, entered the short code, and authorised Hermes. The session belongs to Hermes and is stored in the mounted ~/.hermes data; it does not replace the login used by Codex CLI or VS Code.

An important billing boundary remains: a ChatGPT subscription and OpenAI API credit are separate products. This OAuth route uses the supported ChatGPT/Codex sign-in path. If you have an API key, setting OPENAI_API_KEY would use API billing instead.

The DNS error that was not a permanent DNS failure

Before I could finish entering the device code, Hermes failed with:

Login failed: [Errno -3] Temporary failure in name resolution

I followed the basic troubleshooting steps and checked whether both the Ubuntu host and the running container could find the OpenAI login address:

getent hosts auth.openai.com
docker exec hermes getent hosts auth.openai.com

Both checks returned addresses, and retrying the login succeeded. I therefore treated the original error as a temporary connection problem rather than a permanent DNS failure.

When I later recreated the container, I kept two public DNS servers as a precaution:

--dns=1.1.1.1 --dns=8.8.8.8

Because the login had already started working, I cannot say that these flags fixed the problem. They were a precaution for my setup and are not something every reader needs to add.

Choosing the default model

I chose GPT-5.6 Sol, medium reasoning, with fast mode off. As of this setup date, OpenAI positions Sol for complex reasoning and coding, Terra as the balanced everyday option, and Luna for high-volume, cost-sensitive work.

For my use, Sol (medium) is a sensible default for planning multi-step tasks or writing code. Terra would be a more economical everyday default if most Telegram requests were short questions or small file operations. The best default is therefore not simply “the largest model”. Sol appeared to consume my subscription allowance quickly, so I will monitor my usage and switch to Terra if I find that I am hitting the limits too often.

Why I switched from Discord to Telegram

I first explored Discord, but it introduced more setup than I wanted: an application, a bot, server membership, permissions and intents. Telegram needed one bot token and one permitted user ID. So, I switched to Telegram.

In Telegram I:

  1. opened @BotFather and ran /newbot
  2. saved the bot token as a secret
  3. used @userinfobot to obtain my numeric Telegram user ID and
  4. reopened the Hermes gateway configuration:
docker exec -it hermes hermes setup gateway

I selected Telegram, supplied the bot token, and allowed only my numeric user ID. I did not enable GATEWAY_ALLOW_ALL_USERS=true. A private agent that can run terminal commands should not answer arbitrary Telegram users.

After restarting the container, I sent /sethome to the bot so that the current chat became the home chat, followed by:

Reply exactly: Telegram is working

It replied with the expected text. That small test proved four separate links at once: Telegram could reach the gateway, the gateway could authenticate me, Hermes could reach the selected model, and the response could travel back to Telegram.

The official Hermes Telegram guide is useful once the relationship between those pieces is clear.

Adding Grok as an optional fallback

I also have a Grok subscription, so I connected xAI OAuth. This is optional: a second provider is useful for model choice. I found that Grok 4.5 is a good model and it has generous usage limits. I thought I would add it here and use it for high-volume work.

docker exec -it hermes hermes auth add xai-oauth --no-browser
docker exec hermes hermes doctor

I completed the displayed browser login from my Mac. A compatible Grok subscription can authorise Hermes without an XAI_API_KEY.

I confirmed the fallback model explicitly using:

docker exec hermes hermes fallback list
No fallback providers configured.

My first attempt to add one omitted TTY allocation:

docker exec hermes hermes fallback add

The working command was:

docker exec -it hermes hermes fallback add

I selected xAI OAuth and the available Grok model, then checked the result and restarted the service:

docker exec hermes hermes fallback list
docker restart hermes

Recreating the container with an isolated workspace

The most important design decision came after the chat tests: what should Hermes actually be able to see?

I created one host directory for work, kept the existing directory for Hermes state, and restricted both directories to my Ubuntu account:

mkdir -p ~/.hermes ~/hermes-workspace
chmod 700 ~/.hermes ~/hermes-workspace
chmod 600 ~/.hermes/.env

I then replaced the original container with the command:

docker stop hermes
docker rm hermes

docker run -d \
  --name hermes \
  --restart unless-stopped \
  --dns=1.1.1.1 \
  --dns=8.8.8.8 \
  --shm-size=1g \
  -e PUID="$(id -u)" \
  -e PGID="$(id -g)" \
  -e HERMES_WRITE_SAFE_ROOT="/opt/data:/workspace" \
  -v ~/.hermes:/opt/data \
  -v ~/hermes-workspace:/workspace \
  nousresearch/hermes-agent gateway run

Recreating the container did not erase the model logins, Telegram token or sessions because /opt/data still points to ~/.hermes on the Ubuntu host. The image is disposable; the mounted data is persistent.

The extra choices have specific purposes:

  • PUID and PGID make files created in the workspace easier to edit as my normal Ubuntu user.
  • --shm-size=1g gives Chromium/Playwright more shared memory for browser tasks.
  • HERMES_WRITE_SAFE_ROOT limits where Hermes’s built-in write_file and patch tools are allowed to write.
  • /workspace is the only general-purpose host folder I deliberately exposed.
  • no -p flags means the dashboard and API are not published on the server’s network interfaces.

I set the terminal backend and the gateway’s default working directory:

docker exec -it hermes hermes config set terminal.backend local
docker exec -it hermes hermes config set terminal.cwd /workspace

docker exec -it hermes hermes config set \
  tool_loop_guardrails.hard_stop_enabled true
docker exec -it hermes hermes config set \
  tool_loop_guardrails.hard_stop_after.exact_failure 5
docker exec -it hermes hermes config set \
  tool_loop_guardrails.hard_stop_after.idempotent_no_progress 5

docker restart hermes

Here, local means “run commands in the Hermes container”. It does not mean that the agent can use the whole Ubuntu host. Interactive CLI sessions can receive a working directory directly, while Telegram uses terminal.cwd, which is why setting /workspace matters.

What Hermes can and cannot access

With this final container, the file-access boundary is:

Location Available inside the container? Why
Ubuntu ~/hermes-workspace Yes, as /workspace Explicit bind mount
Ubuntu ~/.hermes Yes, as /opt/data Persistent configuration, sessions and credentials
Files inside the container image Yes Normal container filesystem
Other Ubuntu home folders and disks No Not mounted into the container
My MacBook disks No SSH access does not mount them
Other Docker containers’ files Not mounted No Docker socket or shared volumes
Ubuntu’s Docker daemon No /var/run/docker.sock is not mounted
Network services on the host or elsewhere Possibly The container still has outbound network access

This is useful protection, but it is not complete isolation. The safe-root setting only limits the built-in write_file and patch tools. Terminal commands can still read or change anything mounted inside the container, including Hermes’s own credentials in /opt/data, such as .env and auth.json. The container cannot see unmounted host files, but it can still make outbound network connections.

Following the Hermes security guidance, I therefore keep Telegram restricted to my user ID, avoid unrelated secrets in ~/hermes-workspace, and leave the Docker socket unmounted because it would give Hermes much broader control of the server.

If I later need a project on another disk, I will mount only that project, preferably read-only for inspection:

-v /path/on/ubuntu/project:/workspace/project:ro

I will not mount /, /home or a broad data disk merely for convenience.

Opening the same agent from macOS

Typing the full SSH and Docker command every time quickly became tedious. In ~/.zshrc on my MacBook, I added a shell function. The host and user below are placeholders; my actual connection uses a private Tailscale address.

# Hermes Agent — remote Ubuntu server
hermes() {
  ssh -t ubuntu-user@server-address \
    'docker exec -it -w /workspace -e HERMES_TUI_THEME=light hermes hermes'
}

After reloading the shell:

source ~/.zshrc
hermes

This does not install Hermes on the Mac. It opens a TTY over SSH, then opens a second TTY inside the existing container. The process, sessions and files remain on Ubuntu.

Naming the function hermes is convenient now, but it shadows any local binary with the same name. If I later install Hermes locally, I will rename the function to hermes-remote or something similar.

The strange ]11;rgb:... text

The nested terminal initially printed something similar to:

❯ ]11;rgb:fbfb/f3f3/dbdb

That is a leaked terminal background-colour response—OSC 11—not output from the model. The Hermes TUI queried the terminal colour through nested pseudo-terminals, and part of the response became visible text.

Forcing the TUI theme fixed it. I kept the environment variable in the macOS function and also recorded the preference in Hermes:

docker exec -it hermes hermes config set HERMES_TUI_THEME light

The end-to-end file test

A chat reply proves connectivity, but it does not prove that the Mac, Telegram and Ubuntu host are looking at the same workspace. I tested that explicitly.

First, I opened the remote TUI with hermes on my Mac and asked:

Create /workspace/hermes-test/README.md containing exactly:
Hermes wrote this file on the Ubuntu server.
Then read it back to me.

Next, from a separate Mac terminal, I checked the host-side bind mount:

ssh ubuntu-user@server-address \
  'ls -l ~/hermes-workspace/hermes-test && \
   cat ~/hermes-workspace/hermes-test/README.md'

Then I asked the Telegram bot to read /workspace/hermes-test/README.md. Finally, I restarted the disposable container and asked again:

ssh ubuntu-user@server-address 'docker restart hermes'

The file remained available from both entry points. That test established the behaviour I cared about: one agent on Ubuntu, two remote interfaces, one persistent and limited workspace.

Starting real work

With the infrastructure in place, I will begin with smaller tasks, resolve any issues I encounter, and monitor model usage before trusting Hermes with larger jobs. I am excited to explore what it can do and will report what I learn in future posts.

References