Back to articles
AI Agents

Install Hermes Agent on a VPS with Docker: the complete guide

How I installed Hermes Agent (the OpenClaw Killer) on a VPS: SSH connection, OAuth, DNS configuration, HTTPS reverse proxy and the real production pitfalls.

Daryl Ngako

Install Hermes Agent on a VPS with Docker: the complete guide

What is an autonomous AI agent? It is a program to which you give an objective, not a series of instructions. He decides the steps himself, uses tools (web, files, terminal, code) and advances alone until the result. Where a chatbot responds, an agent acts.

Hermes is exactly that except you can run it on your own machine, which you control from start to finish. And that's where it gets tricky: SSH connection, authentication that expires every hour, DNS, file permissions... so many pitfalls that the tutorials gloss over.

In this guide, I show you how I deployed Hermes Agent (from Nous Research) on an Ubuntu VPS with Docker, from the first SSH connection to the agent responding in a chat interface accessible via your domain name. You will learn how to connect to the server, containerize Hermes, connect it to an inference provider via OAuth, configure your DNS and expose it behind an HTTPS reverse proxy.

This is feedback from the field, not a theoretical doc. Every pitfall I describe, I have encountered and solved for real.

What is Hermes Agent?

Hermes Agent is an autonomous and open source AI agent developed by Nous Research, which learns, memorizes and improves automatically from session to session. Where a traditional chatbot just responds, Hermes can search the web, read and write files, run code in a terminal, generate images, and much more.

Imagine the difference between a colleague who gives you advice over the phone and a colleague who sits at your desk and does the work. The first is a raw LLM. The second is an agent.

Concretely, Hermes connects to an inference provider (here Nous Portal, which provides access to more than 300 models such as Claude Sonnet, GPT, etc.) and adds a whole layer of orchestrated tools. The project is developed by Nous Research, and its official documentation can be found at hermes-agent.nousresearch.com.

Hermes Agent website

Hermes Agent vs OpenClaw

The other big name in the niche is OpenClaw (formerly Moltbot): an open source agent that lives in your messaging services (WhatsApp, Telegram, Slack, etc.), with a system of community skills and a heartbeat that makes it act on its own.

I chose Hermes for three reasons:

  • Speed ​​of execution: Hermes is significantly faster than OpenClaw, which strongly motivated the migration.

  • Different intelligence: It uses Python a lot (rather than Node.js), which gives it a different and sometimes more adapted layer of intelligence.

  • Self-improvement via skills: Hermes can create, modify and learn new "skills" through use (Gmail, Telegram, etc.). He even understands and modifies his own code.

  • Structured memory system: It has memory.md and user.md files with its own context management (context files, @references), which allows it to better remember the user's preferences and habits.

Hermes Agent website

Why containerize it on a VPS?

Running Hermes locally on your machine is good for testing. But for serious use, you want it to run constantly, survive reboots, and be accessible from anywhere. This is exactly the role of a VPS + Docker.

Here are the concrete benefits of this approach:

  • 24/7 availability: your agent is running even when your laptop is closed.
  • Isolation: Docker encapsulates Hermes and its dependencies, without polluting the host system.
  • Reproducibility: your Dockerfile describes the exact environment, you can redeploy it elsewhere in one command.
  • Accessibility: behind a domain name and a reverse proxy, you access your agent from any browser.

Step 1: Connect to the VPS using SSH

Before touching Docker, you need to access your server. The connection is made using SSH (Secure Shell), the standard protocol for controlling a remote machine from the command line.

Your host provided you with three pieces of information: the IP address of the VPS, a username (often ubuntu or root), and either a password or an SSH key. I strongly recommend the SSH key, much more secure than a password.

To log in with a private key:

ssh -i /path/to/your_private_key ubuntu@your.ip.address

On Windows, the path looks like C:\Users\TonNom\.ssh\private_key. On Mac or Linux, rather ~/.ssh/id_rsa.

If this is your first connection, SSH asks you to confirm the server fingerprint, type yes. Once connected, your prompt changes to display the hostname of the server, for example:

ubuntu@my-vps:~$

The $ at the end of the prompt indicates that you are a normal user. A # would indicate that you are root. Work as a normal user (ubuntu) as much as possible and only use sudo when necessary.

Check that Docker is installed on the VPS:

docker --version
docker compose version

If Docker is not there, install it via the official script:

curl -fsSL https://get.docker.com | sh

Now that you are connected and Docker is running, we can build our image.

Step 2: Prepare the Dockerfile

The idea is to start with a lightweight Python image and install Hermes via pipx (which isolates the tool in its own environment). We also add the static Docker binary, essential if you want your agent to be able to create sandbox containers later.

Here is the Dockerfile that I use:

Dockerfile
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
        curl nodejs npm && \
    rm -rf /var/lib/apt/lists/*

# Docker CLI (static binary) for the sandboxed terminal backend
RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
    | tar -xz -C /usr/local/bin --strip-components=1 docker/docker

RUN pip install --no-cache-dir pipx && \
    pipx install hermes-agent && \
    pipx inject hermes-agent aiohttp fastapi uvicorn ptyprocess "mcp>=1.27" httpx-sse langfuse && \
    ln -s /root/.local/bin/hermes /usr/local/bin/hermes

WORKDIR /root

COPY start.sh /start.sh
RUN chmod +x /start.sh

EXPOSE 8642 9119
CMD ["/start.sh"]

Some key points about this file:

  • pipx inject adds additional dependencies in the Hermes environment. Without aiohttp, fastapi, and uvicorn, the API server will not start. This is one of the most common errors when launching for the first time.
  • Static Docker binary is downloaded directly from the official Docker repository. This will be useful if you want the agent to execute code in isolated containers.
  • EXPOSE 8642 9119 declares two ports: 8642 for the API server (OpenAI compatible), 9119 for the web dashboard.

Step 3: The startup script

The CMD in the Dockerfile points to a start.sh. This script launches the dashboard in the background, then the gateway (the API server) in the foreground:

start.sh
#!/bin/sh
umask 0000
# Dashboard in the background
hermes dashboard --host 0.0.0.0 --port 9119 --no-open --insecure --skip-build &
# Gateway in the foreground (includes the API server)
exec hermes gateway

The umask 0000 deserves an explanation. By default, Hermes (which runs as root in the container) creates its files in 0600, therefore readable only by root. If you share a folder with other tools, they will not be able to read these files. The umask 0000 forces creation into permissive mode.

This will save you hours of debugging on mysterious "Permission denied".

Step 4: Docker-compose

We assemble everything in a docker-compose.yml. Here, I connect Hermes to an existing Docker network (the one where my other services run) and I mount the volume of the Hermes config.

docker-compose.yml
services:
  hermes-api:
    build: .
    container_name: hermes-api
    restart: unless-stopped
    expose:
      - "8642"
      - "9119"
    volumes:
      - ./auth:/root/.hermes
    networks:
      - n8nnet

networks:
  n8nnet:
    external: true
    name: my-docker-network

The ./auth:/root/.hermes mount is crucial: it persists the configuration and authentication of Hermes outside the container. Without this, each time you restart you would lose your connection to the inference provider.

Rule to remember: a modification of docker-compose.yml (adding a volume, a port) requires recreating the container with docker compose up -d --force-recreate. A simple restart is not enough to apply a new volume.

Step 5: Connect Hermes to an inference provider

This is where it gets interesting. Hermes needs a brain, that is, an inference model. And you have two main ways to give it to him: go through Nous Portal (the native Hermes supplier), or directly connect your own AI subscription (Claude or Codex). I tested both, here is the honest feedback.

Option A: We Portal (my first approach)

Nous Portal is the "in-house" Hermes provider: a single OAuth login and you have access to more than 300 models (Claude, GPT, etc.), billed per use (per token consumed).

Runs the authentication command in the container:

docker exec -it hermes-api hermes auth add nous --type oauth --no-browser

Authentication for Hermes in container

The --no-browser flag is essential on a headless server: it gives you a URL to open on your own machine, you authenticate, then you paste the return code.

Once connected, select your default model:

docker exec -it hermes-api hermes model

Agent model choice

Check that everything is in place:

docker exec hermes-api hermes portal info

You should see ✓ logged in and the list of available tools (web search, image generation, etc.).

Checking Hermes Agent connection

Why I ended up switching. Us Portal charges for each token consumed. With an autonomous agent running in a loop, making tool calls and ruminating in the background, the bill climbs very quickly. In a few days of sustained use, the cost had become unreasonable in my case. So I switched to a fixed cost solution: a subscription that I was already paying for as a flat rate.

Option B: Connect your Claude or Codex subscription

The idea: instead of paying by token, you reuse a flat-rate subscription (Claude Pro/Max, or ChatGPT via Codex). Hermes authenticates in OAuth against your account, exactly as Claude Code or Codex CLI would do, and consumes your subscription quota instead of a credit charged per usage. Result: a predictable bill, capped each month.

To connect a Claude subscription:

docker exec -it hermes-api hermes auth add anthropic --type oauth --no-browser

To connect Codex (ChatGPT subscription):

docker exec -it hermes-api hermes auth add openai --type oauth --no-browser

As with Nous Portal, the --no-browser gives you a URL to open on your machine. You validate access from your Claude account (or ChatGPT), you paste the returned code, and Hermes writes the tokens in the same auth.json (we'll come back to it right after).

Then select the model corresponding to your subscription, then check the connection:

docker exec -it hermes-api hermes model
docker exec hermes-api hermes portal info

Which choice to make? Us Portal is unbeatable for getting started quickly and testing lots of models without commitment. But as soon as your agent runs continuously, a fixed cost subscription (Claude or Codex) becomes much more predictable on the bill side. This is exactly the path I followed: Nous Portal to discover, Claude subscription for production.

Where authentication lives: the auth.json file

When you connect using OAuth, Hermes stores your identifiers in a file auth.json, inside /root/.hermes/. This file contains two important things: an access token (the token used to call the API) and a refresh token (the token used to regenerate the access token when it expires).

The OAuth access token has a short lifespan, generally a few hours. After this period, it becomes invalid. To avoid having to manually reconnect every hour, Hermes uses the refresh token to automatically rewrite a new access token in auth.json. It is this refresh mechanism that allows your agent to run continuously without intervention.

The trap that breaks the refresh (and how I solved it)

First real obstacle I encountered: after a few hours, the agent had an authentication error, and the automatic refresh failed with a message like "Device or resource busy".

The cause was subtle. In my first compose, I mounted the auth.json file alone as a Docker volume, like ./auth.json:/root/.hermes/auth.json. The problem: When Docker mounts an individual file, it locks it in place. However, the Hermes refresh mechanism does not just modify the content of the file, it does an atomic rewrite (it writes a temporary file then replaces the old one with a rename). And you can't overwrite a file that Docker has mounted individually. Hence the “Device busy”, and the refresh which repeatedly fails.

The solution is to mount the full parent folder, not the file:

volumes:
  - ./auth:/root/.hermes      # ✅ the entire directory
  # NOT ./auth/auth.json:/root/.hermes/auth.json  ❌ the file alone

By mounting the entire /root/.hermes folder, Hermes can create its temporary file and make the rename inside the mounted folder, without conflict with Docker. The refresh then works perfectly, and your token regenerates on its own indefinitely.

Rule to remember: with Docker, always mount the config folder, never an isolated config file that must be rewritten by the application. This is valid for Hermes, but also for many other tools that do atomic rewriting.

If you have already found yourself in the broken state (file mounted alone), correct it then force a clean reconnection:

docker exec -it hermes-api hermes auth add nous --type oauth --no-browser

The new login rewrites a valid auth.json to the correctly mounted folder.

Step 6: Expose the agent with a reverse proxy and HTTPS

Your agent is running, but it is only accessible from inside the Docker network. To access it via a domain name with HTTPS, two things must be put in place: point your domain to your VPS (DNS), then route the traffic to the right container (the reverse proxy).

Configure the DNS of your domain name

Before a name like dash.mondomaine.com leads to your server, you need to create a DNS record that associates this name with the IP address of your VPS. This is what lets the browser know where to go when someone types in your domain.

Go to the DNS management interface of your registrar (the place where you purchased your domain, such as OVH, Cloudflare, Namecheap, Infomaniak) and create a type A record:

TypeName (subdomain)Value (target)
Adashyour.vps.ip.address
Achatyour.vps.ip.address

Type A associates a name with an IPv4 address. The "Name" is the subdomain you want (dash will give dash.mondomaine.com), and the "Value" is the IP of your VPS, the same one you use to connect via SSH.

If you want to expose multiple services (dashboard, chat interface, etc.), create an A record per subdomain. Alternative tip: a wildcard * record that points all the subdomains to your IP at once, handy if you plan to add a lot.

Warning: DNS propagation is not instantaneous. Count from a few minutes to a few hours depending on your registrar.

You can check that your recording is active with the dig command from your VPS:

dig +short dash.mondomaine.com

If it returns the IP of your VPS, the DNS is propagated and you can move on. If that doesn't return anything, wait, the record is not yet propagated.

Set up the Caddy reverse proxy

Once the DNS is in place, I use Caddy as a reverse proxy. Its big advantage: it automatically obtains and renews Let's Encrypt HTTPS certificates, without manual configuration. As soon as your DNS points correctly, Caddy detects the domain and generates the certificate on its own.

Here is a typical Caddy block to display the dashboard, protected by basic authentication:

Caddyfile
dash.mondomaine.com {
    basic_auth {
        admin <hash_bcrypt_du_mot_de_passe>
    }
    reverse_proxy hermes-api:9119
}

To generate the bcrypt hash of your password:

docker exec caddy caddy hash-password --plaintext "your-password"

After modifying the Caddyfile, reload the config without stopping the service:

docker exec caddy caddy reload --config /etc/caddy/Caddyfile

The case of WebSocket interfaces

If you're exposing an interface that uses WebSockets (like an Obsidian web instance or some real-time UI), the simple reverse_proxy may not be enough. Adds headers that preserve the origin:

Caddyfile
obsidian.mondomaine.com {
    basic_auth {
        admin <hash_bcrypt>
    }
    reverse_proxy obsidian:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }
}

I struggled on this point: the page loaded halfway then displayed “Loading error”. The HTTPS certificate was obtained, the container was responding, but the WebSocket handshake was failing. The header_up fixed the problem.

Step 7: Connect a chat interface

To chat with your agent in a real chat (rather than command line), Open WebUI is perfect. It consumes the OpenAI compatible API that Hermes exposes.

In Open WebUI, go to Admin Settings → Connections → Add Connection and enter:

FieldValue
URLhttp://hermes-api:8642/v1
KeyYour Hermes API key (in /root/.hermes/.env)

Added connection in Open WebUI

Your agent will then appear in the models drop-down menu. Select him, and you can talk to him. The difference with a raw model? When you ask it to read a file or search the web, it actually uses its tools.

Open WebUI interface

Attention: do not confuse the agent (which has the tools) with a raw model connected in parallel. If you select a raw model in the menu, it will tell you "I don't have access to the files". Always check that it is your Hermes agent who is selected.

The real pitfalls you will encounter

Here are the obstacles I encountered in production, with their solutions. This is the part that tutorials always forget.

The disk that saturates

Docker stores its images and containers in /var/lib/docker, on the system disk. If your VPS has a small system disk (mine was less than 20 GB), you saturate quickly, especially when Hermes creates sandbox containers.

The solution: move Docker storage to your data volume. Creates or edits /etc/docker/daemon.json:

/etc/docker/daemon.json
{
  "data-root": "/mnt/data/docker"
}

For containerd, also adjusts /etc/containerd/config.toml to point root to /mnt/data/containerd. After restarting the daemon, your system disk breathes.

Permissions on shared files

Hermes runs as root, but other tools (Obsidian, your SSH user) often run in UID 1000. When Hermes writes to a shared folder, the files belong to root and are unreadable by others.

The quick fix:

sudo chmod -R 777 ~/hermes-proxy/vault
sudo chown -R 1000:1000 ~/hermes-proxy/vault

The permanent fix is ​​the umask 0000 in the start.sh that we saw above. It causes new files to be created in world-readable mode from the start.

Hermes orders always go through the container

Small reminder that wasted my time: if you installed Hermes only in the container (to save space), the hermes command does not exist on the host. You get a command not found. All orders must go through:

docker exec -it hermes-api hermes [commande]

All Hermes Agent config

Summary

Here are the essential steps to deploy Hermes on your VPS:

StepKey command
Connect to VPSssh -i private_key ubuntu@your.ip
Building the imagedocker compose up -d --build
Authenticate Providerhermes auth add nous --type oauth --no-browser
Choose modelhermes model
Check Statushermes portal info
Check a DNSdig +short dash.mondomaine.com
Recreate after edit composedocker compose up -d --force-recreate

And the pitfalls to keep in mind:

  • Mount the folder .hermes, never the isolated auth.json file (otherwise the OAuth refresh breaks).
  • Add umask 0000 to avoid permissions issues.
  • Wait for DNS propagation before Caddy can generate the HTTPS certificate.
  • Move Docker storage to a large volume if your system disk is small.
  • For WebSocket UIs behind Caddy, add header_up.

You now have an autonomous AI agent running on your own server, accessible via SSH for the admin and via a chat interface on your domain, with access to tools. This is the solid foundation.

But a single agent, however capable it may be, remains limited: it does everything itself, serially. The real rise in power is when you go from a single agent to an organization of agents who collaborate. Imagine an orchestrator who receives an objective, divides it into tasks, and distributes them to dozens of specialists (a research agent, a writing agent, an agent who checks the work of others), all coordinated by a shared kanban board.

This is exactly what I show you in the next article: how to build an army of 48 AI agents on Hermes, with an orchestration system, delegation via kanban, and tasks planned independently by cron. You will discover the profile system, the role of the SOUL.md file, and the tough pitfalls that I had to resolve (the s6 supervisor limit, bot conflicts, the environment that breaks delegation).

If this guide allowed you to lay the foundation, the next one shows you how to build the building on it.