WSL (Ubuntu 24.04) • Docker • Tailscale Funnel • WordPress/WooCommerce • Telegram Bot Gateway
HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1 fix for WSL hang.<user>, <YOUR_TELEGRAM_USER_ID>,
<BOT_TOKEN>, <your-agentmail-inbox>, and
<your-tailscale-host>.ts.net are placeholders
that stand in for the real values on your own machine — usernames, user/chat
IDs, bot tokens, hostnames, and home paths differ on every install and have been
removed for privacy. Replace each <placeholder> with the
matching value for your system before running any command.This document captures the complete Hermes Agent setup running on a Windows machine via WSL2. It is designed so that if the entire environment is wiped, you can rebuild from scratch by following these instructions step by step.
| Component | Value |
|---|---|
| Host OS | Windows (WSL2 guest) |
| WSL Distro | Ubuntu 24.04.1 LTS |
| WSL Kernel | 6.18.33.2-microsoft-standard-WSL2 |
| Hermes Version | v0.21.0 (2026.8.31) |
| Docker | Docker Engine 29.1.3 + Compose 2.40.3 |
| Tailscale Hostname | |
| LLM Provider | OpenRouter — deepseek/deepseek-v4-flash |
| Email (AgentMail) | |
| Telegram Bot | @ |
| User Home | /home/ |
@BotFatherRun in PowerShell as Administrator:
wsl --install -d Ubuntu-24.04
Inside the WSL terminal:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git unzip gnupg lsb-release ca-certificates
sudo apt install -y python3 python3-venv nodejs npm
git clone https://github.com/NousResearch/hermes-agent.git /home//.hermes/hermes-agent
cd /home//.hermes/hermes-agent
python3 -m venv venv
source venv/bin/activate
pip install -e .
hermes init
hermes setup
Follow the interactive prompts to set OpenRouter as the provider with model deepseek/deepseek-v4-flash.
hermes --version
# Should print: Hermes Agent v0.21.0
Set in /home/:
OPENROUTER_API_KEY=sk-or-v1-YOUR_OPENROUTER_KEY
Located at /home/. Key settings:
model:
default: deepseek/deepseek-v4-flash
provider: openrouter
base_url: https://openrouter.ai/api/v1
agent:
max_turns: 150
reasoning_effort: medium
platforms:
telegram:
enabled: true
home_channel:
platform: telegram
chat_id: ""
name:
user_id: ""
mcp_servers:
agentmail:
url: "https://mcp.agentmail.to/mcp"
headers:
x-api-key: "${AGENTMAIL_API_KEY}"
AgentMail provides email for AI agents. Inbox:
# Set in .env:
AGENTMAIL_API_KEY=your_agentmail_key
sudo apt install -y postgresql-16 postgresql-16-pgvector
sudo -u postgres createuser app -P
sudo -u postgres createdb app -O app
sudo -u postgres createdb ai -O app
Credentials stored in .env:
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=app
DATABASE_URL=postgresql://app:[email protected]:5432/app
DATABASE_URL_AI=postgresql://app:[email protected]:5432/ai
/home/ contains:
OPENROUTER_API_KEY=sk-or-v1-...
TELEGRAM_BOT_TOKEN= (from @BotFather)
TELEGRAM_ALLOWED_USERS=
HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1
TELEGRAM_HOME_CHANNEL=
TELEGRAM_WEBHOOK_SECRET= (32 hex chars)
AGENTMAIL_API_KEY=...
SUDO_PASSWORD=...
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=app
POSTGRES_PASSWORD=...
DATABASE_URL=postgresql://app:[email protected]:5432/app
DATABASE_URL_AI=postgresql://app:[email protected]:5432/ai
HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1 is REQUIRED in WSL. Without it, the Telegram gateway hangs forever on "Connecting to Telegram (attempt 1/8)" due to a DNS-over-HTTPS fallback IP discovery issue specific to WSL.
mkdir -p /home//theblessedhoneybee-wp
cd /home//theblessedhoneybee-wp
File: /home/
services:
db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_ROOT_PASSWORD: root
volumes:
- db_data:/var/lib/mysql
wordpress:
depends_on:
- db
image: wordpress:latest
restart: always
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
volumes:
db_data: {}
wp_data: {}
cd /home//theblessedhoneybee-wp && docker compose up -d
docker compose ps
# Expect: theblessedhoneybee-wp-wordpress-1 and theblessedhoneybee-wp-db-1
docker exec theblessedhoneybee-wp-wordpress-1 bash -lc 'curl -sSfL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp && chmod +x /usr/local/bin/wp && wp --info'
docker exec theblessedhoneybee-wp-wordpress-1 wp core install \
--path=/var/www/html --allow-root \
--url=http://localhost:8080 \
--title="The Blessed Honey Bee" \
--admin_user=admin --admin_password= \
[email protected]
docker exec theblessedhoneybee-wp-wordpress-1 wp plugin install woocommerce --activate --allow-root
docker exec theblessedhoneybee-wp-wordpress-1 wp option update woocommerce_currency AUD --allow-root
docker exec theblessedhoneybee-wp-wordpress-1 wp option update permalink_structure "/%postname%/" --allow-root
docker exec theblessedhoneybee-wp-wordpress-1 wp rewrite flush --allow-root
Theme: blessed-honeybee (active custom theme). Install by copying to wp-content/themes/:
docker cp /path/to/theme theblessedhoneybee-wp-wordpress-1:/var/www/html/wp-content/themes/blessed-honeybee/
docker exec theblessedhoneybee-wp-wordpress-1 wp theme activate blessed-honeybee --allow-root
Since the site is exposed via Tailscale Funnel, update the WordPress URL:
docker exec theblessedhoneybee-wp-wordpress-1 wp option update siteurl "http://.ts.net" --allow-root
docker exec theblessedhoneybee-wp-wordpress-1 wp option update home "http://.ts.net" --allow-root
docker exec theblessedhoneybee-wp-wordpress-1 wp search-replace 'http://localhost:8080' 'http://.ts.net' --allow-root --all-tables
| Component | Version |
|---|---|
| WordPress | 7.1 |
| WooCommerce | 11.0.1 |
| MySQL | 8.0 |
| Theme | blessed-honeybee (custom) |
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Follow the browser auth link to connect to your tailnet
sudo tailscale set --operator=$USER
# Now you can run 'tailscale funnel' without sudo
tailscale funnel --bg --set-path=/ http://127.0.0.1:8080
tailscale funnel --bg --set-path=/telegram http://127.0.0.1:8443
tailscale funnel --bg --set-path=/n8n http://127.0.0.1:5678
tailscale funnel --bg --set-path=/affine http://127.0.0.1:3010
tailscale funnel status
Expected output:
https://.ts.net (Funnel on)
|-- / proxy http://127.0.0.1:8080 (WordPress)
|-- /n8n proxy http://127.0.0.1:5678 (n8n)
|-- /affine proxy http://127.0.0.1:3010 (AFFiNE)
|-- /telegram proxy http://127.0.0.1:8443 (Telegram Gateway)
https://.ts.net:8443 (Funnel on)
|-- / proxy http://127.0.0.1:8443
| Public URL | Path | Destination | Service |
|---|---|---|---|
| *. | / | :8080 | WordPress |
| *. | /n8n | :5678 | n8n |
| *. | /affine | :3010 | AFFiNE |
| *. | /telegram | :8443 | Telegram Gateway |
| *. | / | :8443 | Telegram Gateway (direct) |
@BotFather/newbot, choose a name (e.g. "@)@userinfobot on Telegram/start — get numeric ID: TELEGRAM_BOT_TOKEN=
TELEGRAM_ALLOWED_USERS=
HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1
TELEGRAM_HOME_CHANNEL=
TELEGRAM_WEBHOOK_SECRET= # openssl rand -hex 32
hermes gateway start
# Or restart after config changes:
hermes gateway restart
ss -tlnp | grep 8443
# Should show python process listening on :8443
curl -s "https://api.telegram.org/bot$(grep TELEGRAM_BOT_TOKEN /home//.hermes/.env | cut -d= -f2)/getWebhookInfo"
Expected: JSON with url pointing to https:// and no last_error field.
curl -s "https://api.telegram.org/bot$TOKEN/setWebhook?url=https://.ts.net/telegram&secret_token=$WEBHOOK_SECRET"
HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1 environment variable. If the gateway cycles with exit code 75 (TEMPFAIL), check that this variable is set.
File: /home/
[Unit]
Description=Hermes Agent Gateway - Messaging Platform Integration
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=/home//.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway run
WorkingDirectory=/home//.hermes
Environment="PATH=/home//.hermes/hermes-agent/venv/bin:..."
Environment="VIRTUAL_ENV=/home//.hermes/hermes-agent/venv"
Environment="HERMES_HOME=/home//.hermes"
Environment="HERMES_SUPERVISED_CHILD=1"
Restart=always
RestartSec=5
RestartForceExitStatus=75
RestartPreventExitStatus=78
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=70
[Install]
WantedBy=default.target
systemctl --user enable hermes-gateway.service
systemctl --user start hermes-gateway.service
systemctl --user status hermes-gateway.service
File: /home/
#!/usr/bin/env bash
# Checks BOTH Tailscale connectivity AND funnel route.
set -uo pipefail
TS=$(command -v tailscale || /usr/bin/tailscale)
HOST=$(hostname -s 2>/dev/null || hostname)
# Step 1: Check Tailscale is connected
if ! "$TS" status 2>&1 | grep -q "$HOST.*online"; then
sudo -n "$TS" up 2>/dev/null || sudo "$TS" up
sleep 5
fi
# Step 2: Check funnel route
STATUS=$("$TS" funnel status 2>&1)
if echo "$STATUS" | grep -q "/telegram proxy http://127.0.0.1:8443"; then
echo "OK: /telegram funnel running"
exit 0
fi
echo "FUNNEL DOWN -> attempting restart"
OUT=$(sudo -n "$TS" funnel --bg --set-path=/telegram http://127.0.0.1:8443 2>&1)
RC=$?
if [ $RC -eq 0 ] && echo "$OUT" | grep -vq "Access denied"; then
echo "RESTARTED via sudo: $OUT"
exit 0
fi
OUT2=$("$TS" funnel --bg --set-path=/telegram http://127.0.0.1:8443 2>&1)
RC2=$?
if [ $RC2 -eq 0 ] && echo "$OUT2" | grep -vq "Access denied"; then
echo "RESTARTED (no sudo): $OUT2"
exit 0
fi
echo "COULD NOT RESTART"
exit 1
Runs every 5 minutes via Hermes internal cron system (no system crontab). Configured in /home/:
{
"id": "be5f0ae94275",
"name": "Funnel Watchdog",
"prompt": "Restart the Tailscale Funnel tunnel if it went down...",
"no_agent": true,
"script": "ensure_tailscale_funnel.sh",
"schedule": { "kind": "interval", "minutes": 5 },
"enabled": true,
"deliver": "local"
}
sudo -n tailscale up (requires password), the funnel restart will fail. To fix: sudo tailscale set --operator=$USER allows most tailscale commands without sudo. For tailscale up, configure passwordless sudo for that specific command via visudo.
After rebuilding, run these checks to confirm everything works:
| # | Check | Command |
|---|---|---|
| 1 | Hermes responds | hermes --version |
| 2 | WordPress accessible | curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 → 200 |
| 3 | WooCommerce active | docker exec theblessedhoneybee-wp-wordpress-1 wp plugin list --allow-root |
| 4 | Tailscale connected | tailscale status | grep "$(hostname).*online" |
| 5 | Funnel routes active | tailscale funnel status |
| 6 | Telegram gateway listening | ss -tlnp | grep 8443 |
| 7 | Webhook registered | curl -s "https://api.telegram.org/bot$TOKEN/getWebhookInfo" | grep -c "last_error" → 0 |
| 8 | Funnel public reachability | curl -X POST https:// → 403 (expected — proves tunnel is live) |
| 9 | Gateway service running | systemctl --user is-active hermes-gateway.service |
| 10 | Watchdog cron scheduled | Check /home/ for Funnel Watchdog |
| 11 | AgentMail configured | Verify inbox exists |
| Symptom | Likely Cause | Fix |
|---|---|---|
| Gateway stuck on "Connecting to Telegram (attempt 1/8)" | DNS-over-HTTPS fallback IP discovery hang in WSL | Set HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1 in .env and restart gateway |
| Gateway exits with code 75 repeatedly | Same as above — WSL fallback IP hang | Verify HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1 is present |
| WordPress returns 500 on first load | WordPress still copying files to volume | Wait 30s, check docker logs theblessedhoneybee-wp-wordpress-1 |
| Funnel shows "Funnel on" but Telegram not reachable | Only root route exists, not /telegram path | Add path: tailscale funnel --bg --set-path=/telegram http://127.0.0.1:8443 |
| After WSL reboot, Tailscale offline | WSL restart doesn't restart Tailscale | sudo tailscale up — or the watchdog handles it |
| Funnel routes show locally but 404 from internet | Edge config desync after Tailscale reconnect | sudo tailscale funnel reset then re-apply all routes |
| Watchdog can't restart funnel | sudo password required | sudo tailscale set --operator=$USER |
| Google Workspace auth expired | Token revoked or expired | Re-run the OAuth setup flow |
| Can't find the Hermes CLI binary | venv not activated | source /home/ or use full path |
These files contain the entire setup. Back them up to preserve the configuration:
| File Path | Purpose |
|---|---|
/home/ | All secrets and environment variables (backup this!) |
/home/ | Hermes agent configuration |
/home/ | Google OAuth token (auto-refreshes) |
/home/ | Google OAuth client credentials |
/home/ | Hermes agent source code (git clone) |
/home/ | Funnel watchdog script |
/home/ | Hermes cron job definitions |
/home/ | Systemd user service for gateway |
/home/ | WordPress + MySQL Docker stack |
/home/ | AFFiNE Docker stack |
/home/ | n8n Docker stack |
.env (secrets), config.yaml, the google_*.json files, and all docker-compose.yml files. WordPress database content lives in Docker volumes — back up with docker exec wp db export or docker run --volumes-from.