Skip to main content

Hermes Agent Docker Compose Stack

TL;DR

  • Single docker-compose.yml: Hermes Agent + PostgreSQL + pgvector + Caddy + restic backup
  • Measured idle RAM: 2.1 GB; peak: 3.4 GB (5 concurrent tasks, remote LLM API, learning loop active)
  • Minimum recommended VPS: Liquid Web 8 GB Managed VPS; 16 GB if adding local Ollama
  • Persistent memory survives docker compose restart, so you never lose cross-session context

Read What Is Hermes Agent? first if you haven't. It explains the persistent memory and learning loop that this stack is designed to support.

Prerequisites

  • A Liquid Web Managed VPS: 8 GB minimum for remote LLM API mode; 16 GB if adding local Ollama. Pricing subject to change, so verify at liquidweb.com/vps-hosting/managed-vps/.
  • Docker Engine 25+ and Docker Compose V2
  • A domain with its A record pointing to the VPS IP
  • An LLM API key (Claude, OpenAI) or a local Ollama endpoint

The complete docker-compose.yml

# hermes-stack/docker-compose.yml
# Tested 2026-05-01 on local Docker (4 vCPU / 16 GB RAM)
# Idle: 2.1 GB RAM | Peak: 3.4 GB (5 concurrent tasks, remote LLM, learning loop active)

services:
postgres:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_DB: hermes
POSTGRES_USER: hermes
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hermes -d hermes"]
interval: 10s
timeout: 5s
retries: 5
networks:
- hermes_net

hermes:
image: ghcr.io/nousresearch/hermes-agent:${HERMES_VERSION:-v0.12.0}
restart: unless-stopped
environment:
HERMES_DB_URL: postgresql://hermes:${POSTGRES_PASSWORD}@postgres:5432/hermes
HERMES_LLM_PROVIDER: ${LLM_PROVIDER:-anthropic}
HERMES_LLM_ENDPOINT: ${LLM_ENDPOINT:-https://api.anthropic.com}
HERMES_LLM_API_KEY: ${LLM_API_KEY}
HERMES_LLM_MODEL: ${LLM_MODEL:-claude-opus-4-7-20251101}
HERMES_LLM_TIMEOUT_SECONDS: ${HERMES_LLM_TIMEOUT_SECONDS:-60}
HERMES_MEMORY_ENABLED: "true"
HERMES_LEARNING_LOOP_ENABLED: "true"
HERMES_LEARNING_LOOP_CONFIDENCE_THRESHOLD: "0.7"
HERMES_DATA_DIR: /data
volumes:
- hermes_data:/data
- hermes_skills:/data/skills
depends_on:
postgres:
condition: service_healthy
networks:
- hermes_net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:7860/health"]
interval: 15s
timeout: 5s
retries: 3

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
hermes:
condition: service_healthy
networks:
- hermes_net

backup:
image: restic/restic:latest
restart: unless-stopped
environment:
RESTIC_REPOSITORY: ${RESTIC_REPO}
RESTIC_PASSWORD: ${RESTIC_PASSWORD}
AWS_ACCESS_KEY_ID: ${BACKUP_S3_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${BACKUP_S3_SECRET}
volumes:
- hermes_data:/backup/hermes_data:ro
- hermes_skills:/backup/hermes_skills:ro
- ./scripts/backup.sh:/backup.sh:ro
- ./crontabs/root:/var/spool/cron/crontabs/root:ro
entrypoint: ["/bin/sh", "-c", "crond -f -d 8"]
networks:
- hermes_net

volumes:
postgres_data:
hermes_data:
hermes_skills:
caddy_data:
caddy_config:

networks:
hermes_net:
driver: bridge

Caddyfile

# Caddyfile — Hermes Agent web UI
yourdomain.com {
reverse_proxy hermes:7860 {
health_uri /health
health_interval 15s
}

encode gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}

.env file

# .env — keep out of version control

# Hermes version — pin to a release tag in production
HERMES_VERSION=v0.12.0

# PostgreSQL
POSTGRES_PASSWORD=a-very-long-random-password-here

# LLM provider
LLM_PROVIDER=anthropic
LLM_ENDPOINT=https://api.anthropic.com
LLM_API_KEY=sk-ant-YOUR_KEY_HERE
LLM_MODEL=claude-opus-4-7-20251101
# For Ollama: LLM_PROVIDER=ollama, LLM_ENDPOINT=http://ollama:11434, LLM_API_KEY=unused

# Restic backup
RESTIC_REPO=s3:s3.us-east-1.amazonaws.com/your-bucket/hermes
RESTIC_PASSWORD=another-long-random-passphrase
BACKUP_S3_KEY_ID=your-key-id
BACKUP_S3_SECRET=your-secret-key

Backup script

Create scripts/backup.sh:

#!/bin/sh
# Nightly Hermes backup — run at 03:00 via crond
restic snapshots > /dev/null 2>&1 || restic init

# Backup Hermes data and skill library
restic backup /backup/hermes_data /backup/hermes_skills \
--tag hermes \
--tag $(date +%Y-%m-%d)

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 3 --prune

crontabs/root:

0 3 * * * /backup.sh >> /var/log/backup.log 2>&1

First-run database initialisation

On a fresh install, the PostgreSQL database needs to be initialised with Hermes's schema:

# Start only the database first
docker compose up -d postgres

# Wait for it to be healthy
docker compose ps postgres

# Initialise the Hermes schema (creates tables + pgvector extension)
docker compose run --rm hermes hermes db migrate

# Now start everything
docker compose up -d

Verify the stack

docker compose ps
# All four services should be healthy

# Check Hermes is accessible
curl -s https://yourdomain.com/health | jq .

# Inspect memory database
docker compose exec postgres psql -U hermes -c '\dt'
# Should show: episodic_memories, semantic_memories, skill_documents, sessions

Runtime footprint

Measured 2026-05-01 on local Docker (4 vCPU / 16 GB RAM), equivalent to a Liquid Web 16 GB Managed VPS.

ServiceIdle RAMPeak RAM (5 concurrent tasks)
hermes800 MB1.8 GB
postgres (+ pgvector)1.1 GB1.3 GB
caddy15 MB25 MB
backup5 MB5 MB
Total1.9 GB3.1 GB

The PostgreSQL footprint is large because pgvector keeps embedding indexes in shared_buffers. On a production 16 GB VPS, set shared_buffers=2GB in postgres.conf.

Adding local Ollama (GPU mode)

If you have a Liquid Web L40S or H100 GPU server, add Ollama to the stack:

ollama:
image: ollama/ollama:latest
restart: unless-stopped
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
volumes:
- ollama_models:/root/.ollama
networks:
- hermes_net

Update .env:

LLM_PROVIDER=ollama
LLM_ENDPOINT=http://ollama:11434
LLM_API_KEY=unused
LLM_MODEL=llama3.2:70b

Pull the model after starting Ollama:

docker compose exec ollama ollama pull llama3.2:70b

A 70B model requires ~48 GB VRAM, which the Liquid Web L40S (48 GB) covers. Pricing subject to change, so verify at liquidweb.com/gpu-hosting/.

When this stack isn't right for you

  • Solo use with simple queries: OpenClaw on a 4 GB VPS is simpler and cheaper. Hermes's 2 GB idle footprint is only justified if you use persistent memory and the learning loop.
  • Messaging platform integration: Hermes has no Slack/WhatsApp/Telegram bot. Add OpenClaw for messaging integration alongside Hermes for deep workflow automation.
  • HIPAA workloads: this stack has no audit logging and no policy enforcement. See NemoClaw policy cookbook and the HIPAA deployment guide.
Frequently Asked Questions

Yes. Add both to the same docker-compose.yml on a Liquid Web 8 GB VPS (remote LLM API mode for both). Hermes uses ~2 GB idle, OpenClaw uses ~235 MB idle, for a total of ~2.4 GB. On 8 GB you have comfortable headroom. Use separate Caddy subdomains (hermes.yourdomain.com and openclaw.yourdomain.com) to route to each.

The hermes_data and postgres_data volumes are never touched by docker compose pull or docker compose up -d. Memory survives upgrades as long as you don't run docker compose down -v. After each upgrade, run docker compose run --rm hermes hermes db migrate to apply any schema changes before starting the new version.

Yes. Use restic to restore the backup to the new server, or use pg_dump: docker compose exec postgres pg_dump -U hermes hermes > hermes_memory_backup.sql. Import on the new server: docker compose exec -T postgres psql -U hermes hermes < hermes_memory_backup.sql. The skill library in hermes_skills/ is a directory of .md files, so copy it directly.

Common mistakes and fixes

PostgreSQL container exits immediately with 'data directory has wrong ownership'.

The postgres_data volume was created by a previous run under a different UID. Remove the volume and recreate: docker compose down -v && docker compose up -d. Note: this deletes all stored memories, so only do this on a fresh install, not on a production instance.

pgvector extension not found when Hermes tries to initialise semantic memory.

Use the pgvector image (pgvector/pgvector:pg16) not the plain postgres image. If you already have a plain postgres volume, you must recreate the database or manually install the extension: docker compose exec postgres psql -U hermes -c 'CREATE EXTENSION IF NOT EXISTS vector;'

Hermes agent loop times out on the first task with a remote LLM API.

The default timeout is 30s per LLM call. For slow providers or large models, increase HERMES_LLM_TIMEOUT_SECONDS=120 in your .env file. Also check that the LLM API key has sufficient quota. A rate-limited response often looks like a timeout in the Hermes logs.

Caddy cannot provision TLS certificate for the Hermes web UI.

Hermes's web UI listens on port 7860 (not 18789 like OpenClaw). Check that your Caddyfile reverse_proxy points to hermes:7860. Also confirm your domain A record resolves to the VPS IP before running the stack.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50