Install Hermes Agent (AI agent) on a VPS
This guide explains how to install Hermes Agent on a HostMyServers VPS. Hermes is an open-source AI agent framework (MIT license) published by Nous Research. Unlike a coding assistant used live on your workstation (Claude Code, OpenCode…), Hermes runs permanently on a server: you talk to it from your messaging app (Discord, Telegram, Slack…), it works autonomously in the background, keeps a memory of your conversations, learns new procedures (skills) and can run scheduled tasks.
The deployment relies on Docker Compose with a hardened configuration, and the web administration interface is reachable only through a VPN (Tailscale), never from the Internet.
This guide is based on the hands-on feedback from the article « Hermes : simple hype ou vraie révolution ? » ("Hermes: just hype or a real revolution?", damyr.fr, in French) and on the official Hermes Agent documentation.
Order a Server
Hermes does not run an AI model locally: it calls remote models (OpenRouter, Anthropic, Nous Portal…). A small VPS is therefore more than enough:
- Performance VPS - Ideal for a personal instance
- NVMe VPS - Excellent value for money
- Eco Dedicated Servers - For several agents/profiles or heavy browser automation
Prerequisites
- SSH access as root or a user with sudo
- Ubuntu 24.04 LTS or Debian 12/13
- An account with a model provider (for example OpenRouter) with an API key
- A Tailscale account (free for personal use) to access the web interface
- A Discord account (or Telegram, Slack…) to chat with the agent
System Requirements
| Component | Minimum | Recommended |
|---|---|---|
| RAM | 2 GB | 4 GB |
| CPU | 1 vCPU | 2 vCPU |
| Storage | 10 GB | 20 GB |
The article's author runs Hermes on a 2 vCPU / 2 GB VPS, but recommends at least 4 GB of RAM as soon as you use browser automation (Playwright/Chromium) or several sub-agents.
Browser consoles (noVNC) mishandle some characters (:, @, =) and can silently corrupt pasted commands and API keys. Use a real SSH session.
Connecting and Updating the System
ssh user@server_ip_address
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl
Installing Docker
The official Docker image is the installation method recommended by Hermes: the image is stateless, all data lives in a single mounted folder (/opt/data), and an update simply means pulling a new image.
Install Docker Engine and the Compose plugin from the official Docker repository:
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
curl -fsSL https://download.docker.com/linux/$ID/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Check the installation:
docker --version
docker compose version
Installing Tailscale (Private Access)
The Hermes web interface lets you administer everything (API keys, configuration, MCP, plugins, running commands through the agent). It must never be exposed to the Internet: instances open on 0.0.0.0 have been compromised by automated scanners (malicious SSH keys installed through the agent). We therefore make it reachable only from your private Tailscale network.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
Open the displayed authentication link to attach the server to your Tailscale account, then get the VPS's Tailscale IP address (100.x.y.z range):
tailscale ip -4
Write this address down: it will be used as the listening address for the web interface. Also install Tailscale on your computer or phone so you can access it.
Docker writes its own iptables rules and bypasses the UFW firewall for published ports. Publishing the port on the Tailscale address (and not on 0.0.0.0) guarantees it cannot be reached from the public interface, whatever the firewall state.
Preparing the Directories
sudo mkdir -p /opt/hermes/data
sudo chown -R 10000:10000 /opt/hermes/data
sudo chmod 0750 /opt/hermes/data
cd /opt/hermes
The container runs the agent as the unprivileged user UID/GID 10000: the data folder must belong to it.
Environment File
Create the /opt/hermes/.env file, which will hold the listening address and the web interface credentials:
sudo nano /opt/hermes/.env
# Tailscale address of the server (output of `tailscale ip -4`) — NEVER 0.0.0.0
HERMES_BIND_ADDR=100.x.y.z
HERMES_DASHBOARD_PORT=9119
# Web interface credentials
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=PASSWORD
HERMES_DASHBOARD_BASIC_AUTH_SECRET=SECRET
# Resource limits (adapt to your VPS: ~1200m for 2 GB, 3g for 4 GB)
HERMES_MEMORY_LIMIT=1200m
HERMES_CPU_LIMIT=1.5
TZ=Europe/Paris
Generate the password and the session secret, then copy them into the file:
openssl rand -base64 24 # -> HERMES_DASHBOARD_BASIC_AUTH_PASSWORD
openssl rand -hex 32 # -> HERMES_DASHBOARD_BASIC_AUTH_SECRET
HERMES_DASHBOARD_BASIC_AUTH_SECRET must be at least 16 bytes long. Below that, the authentication module does not activate and the interface refuses to start with the misleading error "no auth providers are registered". openssl rand -hex 32 is more than enough. Keep this secret stable: changing it invalidates open sessions.
Protect the file, it contains secrets:
sudo chmod 0600 /opt/hermes/.env
Docker Compose Configuration
Create /opt/hermes/compose.yaml:
sudo nano /opt/hermes/compose.yaml
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
command: gateway run
restart: unless-stopped
stop_grace_period: 30s
# No process in the container can acquire new privileges
security_opt:
- no-new-privileges:true
# Drop all default Linux capabilities...
cap_drop:
- ALL
# ...and only give back those needed to initialize the container
cap_add:
- CHOWN
- FOWNER
- DAC_OVERRIDE
- SETUID
- SETGID
- KILL
# Avoids errors caused by too many open files
ulimits:
nofile:
soft: 4096
hard: 8192
ports:
# Web interface published ONLY on the Tailscale address
- "${HERMES_BIND_ADDR}:${HERMES_DASHBOARD_PORT:-9119}:${HERMES_DASHBOARD_PORT:-9119}"
volumes:
# All agent data: config, keys, memory, skills, crons, logs
- ./data:/opt/data
# NEVER mount /var/run/docker.sock: it would grant the equivalent of root on the host
environment:
PUID: "10000"
PGID: "10000"
TZ: "${TZ:-Europe/Paris}"
# Web interface (supervised in the same container as the gateway)
HERMES_DASHBOARD: "1"
HERMES_DASHBOARD_HOST: "0.0.0.0" # inside the container only; on the host side, only Tailscale is exposed
HERMES_DASHBOARD_PORT: "${HERMES_DASHBOARD_PORT:-9119}"
HERMES_DASHBOARD_BASIC_AUTH_USERNAME: "${HERMES_DASHBOARD_BASIC_AUTH_USERNAME}"
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: "${HERMES_DASHBOARD_BASIC_AUTH_PASSWORD}"
HERMES_DASHBOARD_BASIC_AUTH_SECRET: "${HERMES_DASHBOARD_BASIC_AUTH_SECRET}"
# Agent safeguards
HERMES_WRITE_SAFE_ROOT: "/opt/data" # the agent can only write to its data folder
HERMES_YOLO_MODE: "0" # dangerous commands still require approval
shm_size: "256m"
deploy:
resources:
limits:
memory: "${HERMES_MEMORY_LIMIT:-1200m}"
cpus: "${HERMES_CPU_LIMIT:-1.5}"
pids: 512
# Prevents a very chatty agent from filling up the disk
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
The article's author publishes a variant of the official image (DamyrFr/hermes-docker) which notably adds rtk (filters command output to reduce token consumption) and removes setuid binaries. The configuration above reuses its security settings with the official image.
First Launch: the Setup Wizard
Before starting the service, run the interactive wizard (text-mode interface) once. It asks for your model provider and API keys, and offers to configure messaging (gateway):
cd /opt/hermes
sudo docker compose run --rm hermes setup
During the wizard:
- Model provider: choose for example OpenRouter and paste your API key. Alternative:
hermes setup --portalwith a Nous Portal subscription (300+ models and web tools included, no keys to manage). - Tools: enable web search and page extraction (see below).
- Messaging: configure Discord (see next section) — it is the main interface with the agent.
- Obscure settings can be left at their defaults; everything can be changed later.
Keys are stored in /opt/hermes/data/.env and the configuration in /opt/hermes/data/config.yaml.
OpenRouter lets you set a spending limit per API key, which can be reset periodically. It is ideal for a personal pay-as-you-go instance.
Creating the Discord Bot (Gateway)
The gateway is the component that connects Hermes to your messaging app. Discord is one of the most popular options: channels and threads help keep conversations well organized.
- Go to the Discord Developer Portal and click New Application.
- In the Bot tab, enable the following Privileged Gateway Intents then click Save Changes:
- Message Content Intent (required: without it, the bot receives empty messages)
- Server Members Intent (required)
- Still in Bot, click Reset Token and copy the token.
- Invite the bot to your Discord server via the Installation tab (or OAuth2 → URL Generator with the
botscope). - Get your Discord user ID: enable Developer Mode (Settings → Advanced), then right-click your name → Copy User ID.
If you did not do it in the wizard, run the gateway setup:
sudo docker compose run --rm hermes gateway setup
Or add these lines directly to /opt/hermes/data/.env:
DISCORD_BOT_TOKEN=your-bot-token
DISCORD_ALLOWED_USERS=123456789012345678
DISCORD_ALLOWED_USERS (or DISCORD_ALLOWED_ROLES) lists the only people allowed to give orders to the agent. Without this variable, Hermes refuses everyone for security reasons. Only add people you trust: the agent can run commands on the server.
By default, Hermes replies to all your direct messages and, in channels, only when it is @mentioned.
Starting the Service
cd /opt/hermes
sudo docker compose up -d
sudo docker compose logs -f --tail=100
restart: unless-stopped automatically restarts the container after a crash or a VPS reboot (including when the Tailscale interface is not ready yet at boot: Docker retries until it is).
Check that the port is only published on the Tailscale address:
sudo ss -tlnp | grep 9119
The output must show 100.x.y.z:9119 and not 0.0.0.0:9119.
Then send a direct message to your bot on Discord: it should reply.
Accessing the Web Interface
From a device connected to your Tailscale network, open:
http://100.x.y.z:9119
Log in with the credentials defined in /opt/hermes/.env. The interface lets you chat with the agent, view logs, manage configuration, MCP, profiles and plugins, and also provides a Kanban board: you create a task, the agent splits it into sub-tasks assigned to sub-agents, and the cards move forward automatically.
Data Layout
All agent state lives in /opt/hermes/data (mounted on /opt/data in the container):
| Path | Role |
|---|---|
config.yaml | Global settings: models, tools, approvals, delegation |
.env | API keys (model providers, Discord, web tools…) |
auth.json | Authentications (OAuth, messaging platforms) |
SOUL.md | Agent personality (name, tone, way of interacting) |
skills/ | Procedures the agent learns and improves over time |
cron/ | Scheduled tasks |
state.db | Agent memory: sessions, histories… (never edit it or copy it while running) |
Customizing the Agent
Giving It a Personality (SOUL.md)
The SOUL.md file defines the agent's character and the way it expresses itself. Give it a name and a tone: it makes day-to-day conversations more pleasant.
sudo nano /opt/hermes/data/SOUL.md
# Janet
You are Janet, a helpful, concise and slightly mischievous assistant.
You reply in English, get straight to the point and ask for confirmation
before any irreversible action.
Choosing the Models
A good trade-off is to use a fast, inexpensive model for chatting, and a stronger model for autonomous work by sub-agents (delegation). Example configuration in /opt/hermes/data/config.yaml with OpenRouter:
model:
default: deepseek/deepseek-v4.1-flash
provider: openrouter
base_url: https://openrouter.ai/api/v1
api_mode: chat_completions
delegation:
max_iterations: 50
model: z-ai/glm5.3
provider: openrouter
These models are examples taken from the article at the time it was written. The landscape changes fast: choose the ones that fit your budget and needs. You can also switch models from Discord with the /model command.
A Second Agent for Code Review
To avoid a model reviewing its own work, create a separate profile using another model (for example Claude Opus), then ask your main agent to systematically have it review the code before submitting it to you:
sudo docker compose run --rm hermes profile create reviewer
The profile is then configured (model, provider) from the web interface, in the Profiles section.
Web Search
To search the Internet autonomously, the agent uses two tools:
web_search: finds relevant links.duckduckgo-searchworks locally, for free.web_extract: converts one or more pages to Markdown. Tavily offers a key with a free quota.
Enable them from the web interface or with sudo docker compose run --rm hermes tools.
Scheduled Tasks (Crons)
Hermes natively handles scheduled tasks. The simplest way is to request them directly in Discord:
/cron add "every 2h" "Check the server status and alert me if anything is wrong"
/cron add "every 1d" "Give me a DevOps news digest and summarize the 5 most important items"
Usage ideas: automated multi-source monitoring, regular ingestion of an Obsidian vault ("second brain"), periodic reports. By default, a dangerous command triggered by a cron is denied (approvals.cron_mode: deny).
Security: Best Practices
- Web interface: only through Tailscale (or another VPN), never on
0.0.0.0, always with authentication. - Approvals: keep
approvals.mode: smart(default) or switch tomanualinconfig.yaml. Never use--yolo/approvals.mode: offon an agent connected to a messaging app. - Messaging access: limit
DISCORD_ALLOWED_USERSto yourself. - Docker socket: never mount it into the container.
- API keys: use dedicated keys with a spending cap, and be careful about the permissions granted to integrations (GitHub, email, MCP…).
- Firewall: only open SSH on the public interface. See Securing your Linux server.
Useful Commands
| Action | Command (from /opt/hermes) |
|---|---|
| View logs | sudo docker compose logs -f --tail=100 |
| Restart | sudo docker compose restart |
| Stop | sudo docker compose down |
| Rerun the wizard | sudo docker compose run --rm hermes setup |
| Change model | sudo docker compose run --rm hermes model |
| Diagnostics | sudo docker compose run --rm hermes doctor |
Stop the service (docker compose down) before using docker compose run to change the configuration, then start it again with docker compose up -d.
Updating
Since the image is stateless, simply pull the new version and recreate the container:
cd /opt/hermes
sudo docker compose pull
sudo docker compose up -d
Backup
The whole agent (configuration, keys, memory, skills, crons) fits in /opt/hermes/data. The state.db database must not be copied while the agent is running: stop it while the archive is created.
cd /opt/hermes
sudo docker compose stop
sudo tar czf /root/hermes-backup-$(date +%F).tar.gz -C /opt/hermes data .env compose.yaml
sudo docker compose start
Keep these archives off the server: they contain your API keys.
Troubleshooting
The web interface does not start ("no auth providers are registered")
- Check that
HERMES_DASHBOARD_BASIC_AUTH_SECRETis at least 16 bytes long (useopenssl rand -hex 32). - Check that all three
HERMES_DASHBOARD_BASIC_AUTH_*variables are set in/opt/hermes/.env.
The container does not start
- Check the logs:
sudo docker compose logs --tail=200 - Check the data folder permissions:
ls -ln /opt/hermes(owner10000:10000) - Check that
HERMES_BIND_ADDRmatchestailscale ip -4and that Tailscale is running:tailscale status - If a permission error appears during initialization after an update, temporarily comment out the
cap_drop/cap_addblock to confirm, then add only the missing capability.
The Discord bot is online but does not reply
- Check that Message Content Intent is enabled in the Developer Portal.
- Check that your ID is listed in
DISCORD_ALLOWED_USERS. - In a channel, remember to @mention the bot.
- Restart after any change:
sudo docker compose restart
"API key not set"
Rerun the provider configuration: sudo docker compose run --rm hermes model, then restart the service.