Skip to content

Instantly share code, notes, and snippets.

@lfsmoura
Last active July 18, 2026 19:11
Show Gist options
  • Select an option

  • Save lfsmoura/3bc66db0e6c1e321152c21fe279af1a7 to your computer and use it in GitHub Desktop.

Select an option

Save lfsmoura/3bc66db0e6c1e321152c21fe279af1a7 to your computer and use it in GitHub Desktop.
Deploying Hermes Agent (NousResearch) on Dokploy — step-by-step guide

Deploying Hermes Agent on Dokploy

A step-by-step guide to deploying NousResearch Hermes Agent on a VPS using Dokploy, secured with Tailscale and a locked-down firewall.

Prerequisites

  • A VPS with Dokploy installed
  • Dokploy API token (Settings → API Keys in Dokploy UI)
  • A Telegram bot token from @BotFather
  • An LLM API key (OpenAI-compatible endpoint)

0. Secure the VPS with Tailscale + Firewall

The idea: block all inbound ports on the public IP (including SSH), and access everything through Tailscale. This means no attack surface on the public internet — the server is only reachable from your tailnet.

Install Tailscale

# On the VPS
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh

The --ssh flag enables Tailscale SSH, so you can SSH into the server using the Tailscale IP without managing SSH keys or port 22.

Lock down the firewall (UFW)

# Deny all incoming traffic by default
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow only HTTP/HTTPS if you're serving public websites via Traefik
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable
sudo ufw status

Key point: SSH (port 22) is NOT opened. You access the server exclusively via Tailscale SSH (ssh <tailscale-ip> or ssh <hostname> if configured in ~/.ssh/config).

Configure SSH access

Add to your local ~/.ssh/config:

Host my-vps
    HostName 100.x.x.x   # Tailscale IP
    User root

Now you can run commands with:

ssh my-vps "docker ps"
ssh my-vps "ufw status"

Why this matters

Port Public IP Tailscale IP
22 (SSH) Blocked Open (Tailscale SSH)
80 (HTTP) Open Open
443 (HTTPS) Open Open
3000 (Dokploy) Blocked Open
Everything else Blocked Open

Dokploy's UI and API (port 3000) are only accessible from your tailnet. No one on the public internet can reach the admin panel, API, or SSH.

Call the Dokploy API from your local machine

Since Tailscale creates a private network, you can call the Dokploy API directly from your laptop — no need to SSH in first:

TOKEN="your-dokploy-token"
BASE_URL="http://100.x.x.x:3000/api/trpc"

# This works from your local machine, not just the server
curl -s -H "x-api-key: $TOKEN" "$BASE_URL/project.all"

This is actually preferred over SSH + localhost, since Tailscale SSH can occasionally require re-authentication via browser, making SSH commands flaky for scripting.

1. Create Project + Application

TOKEN="your-dokploy-token"
BASE_URL="http://100.x.x.x:3000/api/trpc"

# Create a project
curl -s -X POST "$BASE_URL/project.create" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"name":"hermes","description":"Hermes Agent"}}'

# Create an application (use projectId + environmentId from response above)
curl -s -X POST "$BASE_URL/application.create" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"name":"hermes-agent","projectId":"PROJECT_ID","environmentId":"ENV_ID"}}'

# Configure git source with Dockerfile build
curl -s -X POST "$BASE_URL/application.update" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID","sourceType":"git","customGitUrl":"https://github.com/nousresearch/hermes-agent.git","customGitBranch":"main","buildType":"dockerfile"}}'

# Enable submodules (required by hermes)
curl -s -X POST "$BASE_URL/application.update" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID","enableSubmodules":true}}'

2. Set Up Persistent Volume and Environment

# Create volume so config/sessions survive redeploys
curl -s -X POST "$BASE_URL/mounts.create" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"type":"volume","mountPath":"/opt/data","serviceId":"APP_ID","serviceType":"application","volumeName":"hermes-data"}}'

# Set command to sleep (keeps container alive for interactive setup)
curl -s -X POST "$BASE_URL/application.update" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID","command":"sleep infinity"}}'

# Set env vars
curl -s -X POST "$BASE_URL/application.update" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID","env":"HERMES_HOME=/opt/data\nTERMINAL_ENV=local\nTERMINAL_TIMEOUT=60"}}'

3. Deploy (First Build)

# Trigger deploy — builds Docker image (~3-5 min, large image with Playwright/Chromium)
curl -s -X POST "$BASE_URL/application.deploy" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID"}}'

# Check deployment status
curl -s -H "x-api-key: $TOKEN" \
  --get "$BASE_URL/deployment.allByType" \
  --data-urlencode 'input={"json":{"id":"APP_ID","type":"application"}}'

4. Run Interactive Setup

Once the container is running with sleep infinity, SSH in via Tailscale:

# Find the container
ssh my-vps "docker ps --filter name=your-app-name"

# Run hermes setup (interactive wizard)
ssh my-vps "docker exec -it \$(docker ps -qf name=your-app-name) hermes setup"

The setup wizard will ask you to configure:

  • LLM provider: Choose your provider and set API key + base URL
  • Model: Pick a model your provider supports
  • Telegram: Paste your bot token, set allowed user IDs
  • Personality: Choose a preset or use default

Config is saved to /opt/data/config.yaml and /opt/data/.env.

5. Switch to Production and Redeploy

# Change command to run the Telegram gateway
curl -s -X POST "$BASE_URL/application.update" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID","command":"hermes gateway run"}}'

# Redeploy (reuses existing image, just restarts with new command)
curl -s -X POST "$BASE_URL/application.redeploy" \
  -H "x-api-key: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"json":{"applicationId":"APP_ID"}}'

6. Verify

# Check container is running
ssh my-vps "docker ps --filter name=your-app-name"

# Check logs
ssh my-vps "docker logs \$(docker ps -qf name=your-app-name) --tail 50 2>&1"

Gotchas

  • Dokploy API is tRPC-based: mutations use POST with {"json":{...}} body, queries use GET with URL-encoded input param.
  • Telegram group chats: Disable privacy mode via @BotFather (/setprivacy → Disable), then remove and re-add the bot to the group.
  • Config persists in the hermes-data volume at /opt/data/ — survives redeploys. No need to re-run setup after redeploying.
  • The Docker image is large (Debian + Python + Node + Playwright/Chromium) — first build takes several minutes.
  • hermes setup is interactive — that's why we first deploy with sleep infinity, run setup, then switch to the real command.
  • Prefer API calls over SSH — Tailscale SSH can require periodic browser re-auth, making it unreliable for scripts. Use direct curl to the Tailscale IP for Dokploy API operations.

Dokploy API Quick Reference

Operation Method Endpoint
List projects GET project.all
Get app details GET application.one
Create app POST application.create
Update app POST application.update
Create volume POST mounts.create
Deploy POST application.deploy
Redeploy POST application.redeploy
Check deploys GET deployment.allByType

All endpoints are prefixed with http://<tailscale-ip>:3000/api/trpc/ and require the x-api-key header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment