Hermes' eigenes Auth-Gate (dashboard.basic_auth/HERMES_DASHBOARD_PASSWORD_HASH) engagiert sich laut eigener Fehlermeldung ohnehin nur bei einem 0.0.0.0-Bind, der Dashboard-Container bindet aber fest auf 127.0.0.1 -- das Gate haette hier nie gegriffen. Statt es als "Fallback falls Bind mal wieder wandert" zu behalten, jetzt sauber raus: HERMES_DASHBOARD_PASSWORD_HASH aus .env.example und docker-compose.yml, der auskommentierte dashboard:-Block aus config.yaml.example, README-Setup auf 8 statt 9 Schritte umnummeriert. Caddy (TLS + eigene Basic-Auth) ist damit die einzige noetige Auth-Schicht -- ein Layer statt zwei, weniger Setup, weniger Verwirrung. Stale Kommentare im Caddy-Compose-Block (redeten noch von "zweiter Basic-Auth-Schicht noetig weil unklar ob dashboard.basic_auth greift") ebenfalls korrigiert.
266 lines
14 KiB
YAML
266 lines
14 KiB
YAML
services:
|
|
|
|
# ─── Claude Max API Proxy (fuer Hermes) ─────────────────
|
|
# Gleicher Trick wie beim ARIA-Proxy (/root/ARIA-AGENT/docker-compose.yml,
|
|
# Service "proxy"): das npm-Paket claude-max-api-proxy wrappt die Claude
|
|
# Code CLI (angemeldet mit Claude-Max-Subscription) als OpenAI-kompatiblen
|
|
# /v1/chat/completions-Endpoint. Die Sed-Patches unten sind identisch zu
|
|
# ARIA — noetig damit:
|
|
# - der Server auf 0.0.0.0 bindet (sonst nur localhost im Container)
|
|
# - Claude ohne interaktive Permission-Prompts laeuft (--dangerously-skip-permissions,
|
|
# zwingend fuer non-interactive Container-Betrieb als root)
|
|
# - der System-Prompt komplett ersetzt wird (--system-prompt) statt nur
|
|
# angehaengt — sonst gewinnt Claude Codes eigene "You are Claude Code"-
|
|
# Identitaet bei duennem Kontext
|
|
# - lange Agent-Loops nicht nach 5min timeouten (DEFAULT_TIMEOUT -> 24h)
|
|
# - der Prompt via stdin statt CLI-Arg uebergeben wird (Arg-Laengenlimit/
|
|
# Escaping-Probleme bei langen Konversationen)
|
|
#
|
|
# Adapter-Patches (proxy-patches/*.js) sind 1:1 aus dem ARIA-Proxy
|
|
# uebernommen — sie sind generisch (implementieren nur die
|
|
# OpenAI-tools <-> <tool_call>-Text-Bridge, kein ARIA-Code drin):
|
|
# - openai-to-cli.js: haengt `tools`-Definitionen als Anleitung in den
|
|
# System-Prompt ("gib <tool_call name=X>{json}</tool_call> aus")
|
|
# - cli-to-openai.js: parsed genau dieses Format aus Claudes Antwort
|
|
# wieder raus und baut daraus OpenAI-konforme tool_calls[]
|
|
#
|
|
# BEWUSST NICHT uebernommen: ARIAs routes.js-Patch. Der haengt zusaetzlich
|
|
# an aria-bridge:8090 (Live-Tool-Stream + Not-Aus-Killswitch) und liest
|
|
# /shared/config/models.json — beides ARIA-Bridge-spezifisch. Hier laeuft
|
|
# die NPM-Stock-Version von routes.js (macht die Kern-Route trotzdem,
|
|
# nur ohne die Bridge-Extras). Falls Hermes spaeter auch Live-Stream/
|
|
# Killswitch braucht: sag Bescheid, dann bauen wir das nach.
|
|
hermes-proxy:
|
|
image: node:22-alpine
|
|
container_name: hermes-proxy
|
|
command: >-
|
|
sh -c "apk add --no-cache bash curl &&
|
|
npm install -g @anthropic-ai/claude-code claude-max-api-proxy &&
|
|
DIST=$$(find /usr/local/lib -path '*/claude-max-api-proxy/dist' -type d | head -1) &&
|
|
sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js &&
|
|
sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js &&
|
|
sed -i 's/\"--dangerously-skip-permissions\",/\"--dangerously-skip-permissions\",\"--system-prompt\",options.systemPrompt,/' $$DIST/subprocess/manager.js &&
|
|
sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js &&
|
|
sed -i '/prompt, \\/\\/ Pass prompt as argument/d' $$DIST/subprocess/manager.js &&
|
|
sed -i 's|this\\.process\\.stdin?\\.end();|this.process.stdin?.end(prompt);|' $$DIST/subprocess/manager.js &&
|
|
cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js &&
|
|
cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
|
|
claude-max-api"
|
|
volumes:
|
|
# WICHTIG: eigenes, von ARIA getrenntes Volume fuer die Claude-CLI-Session.
|
|
# Einmalig `docker exec -it hermes-proxy claude` fuer den Browser-Login
|
|
# mit der Claude-Max-Subscription (kann derselbe Account wie ARIA sein —
|
|
# es ist ja dieselbe Subscription, nur eine eigene CLI-Session dafuer).
|
|
- ./hermes-data/claude-config:/root/.claude
|
|
# optional: nur noetig falls Hermes Dateien mit Dir austauschen soll
|
|
- ./hermes-shared:/shared
|
|
- ./proxy-patches:/proxy-patches:ro
|
|
tmpfs:
|
|
# wie bei ARIA: Claude Codes eigenes Auto-Memory nicht persistieren
|
|
- /root/.claude/projects
|
|
environment:
|
|
- HOST=0.0.0.0
|
|
- SHELL=/bin/bash
|
|
- CLAUDE_CODE_BUBBLEWRAP=1 # erlaubt --dangerously-skip-permissions als root
|
|
restart: unless-stopped
|
|
networks:
|
|
- hermes-net
|
|
# Bewusst KEIN ports:-Mapping nach aussen — genau wie bei ARIAs Proxy.
|
|
# Nur Container im selben Docker-Netz (hier: hermes-gateway) sollen ihn
|
|
# erreichen koennen. Das eigentliche Netzwerk-Exposing + Auth macht das
|
|
# Gateway unten, damit hier niemals ein unauthentifizierter Endpoint
|
|
# offen im Netz haengt.
|
|
# Zum manuellen Testen direkt im Container: docker exec -it hermes-proxy curl -s localhost:3456/v1/chat/completions ...
|
|
|
|
# ─── Hermes Gateway (Auth vor dem Claude-Proxy) ────────────
|
|
# Stefan hat entschieden: Hermes Agent selbst laeuft auf DERSELBEN Maschine
|
|
# wie dieser Proxy-Stack (alles zu Hermes gehoerige auf eine Box). Trotzdem
|
|
# bleibt dieses Gateway davor — hermes-proxy selbst hat keine Authentifizierung
|
|
# (genau wie ARIAs Proxy) und ein anderer Container im selben Docker-Host
|
|
# (oder spaeter doch mal ein zweiter Rechner) soll nicht ungefragt an die
|
|
# Claude-Max-Subscription rankommen. Das Gateway (proxy-patches/gateway.js,
|
|
# pure Node, keine Extra-Deps) prueft den Authorization: Bearer <TOKEN>
|
|
# Header, leitet nur bei Match an hermes-proxy weiter. Alles andere -> 401.
|
|
#
|
|
# Der Hermes-Agent-Service unten ist per ${HERMES_GATEWAY_TOKEN} + diesem
|
|
# Gateway vorverdrahtet (siehe hermes-agent-config/config.yaml.example) —
|
|
# kein manuelles config.yaml-Editieren mehr noetig.
|
|
hermes-gateway:
|
|
image: node:22-alpine
|
|
container_name: hermes-gateway
|
|
command: ["node", "/gateway/gateway.js"]
|
|
depends_on:
|
|
- hermes-proxy
|
|
volumes:
|
|
- ./proxy-patches:/gateway:ro
|
|
environment:
|
|
- TOKEN=${HERMES_GATEWAY_TOKEN:?HERMES_GATEWAY_TOKEN muss in .env gesetzt sein - siehe .env.example}
|
|
- UPSTREAM_HOST=hermes-proxy
|
|
- UPSTREAM_PORT=3456
|
|
- LISTEN_PORT=8080
|
|
ports:
|
|
# Seit "alles auf einer Maschine": nur noch auf localhost binden, nicht
|
|
# mehr auf allen Interfaces. Hermes Agent laeuft ja jetzt selbst hier
|
|
# (network_mode: host, siehe unten) und erreicht localhost direkt.
|
|
# Falls Du spaeter doch von einer ANDEREN Maschine ranwillst (z.B. fuer
|
|
# einen mobilen Client direkt gegen Hermes' eigenen API-Server, siehe
|
|
# Kommentar unten bei API_SERVER_*): "127.0.0.1:" vorne weglassen, dann
|
|
# bindet's wieder auf 0.0.0.0 — aber dann auch das Sicherheitshinweis-
|
|
# Kapitel im README zu TLS/VPN beachten.
|
|
- "127.0.0.1:${HERMES_GATEWAY_PORT:-8447}:8080"
|
|
restart: unless-stopped
|
|
networks:
|
|
- hermes-net
|
|
|
|
# ─── Hermes Agent selbst (Nous Research) ───────────────────
|
|
# Offizielle Service-Definition 1:1 aus NousResearch/hermes-agent/docker-compose.yml
|
|
# (nur umbenannt gateway->hermes-agent, dashboard->hermes-agent-dashboard, damit
|
|
# es nicht mit unserem eigenen "hermes-gateway" oben verwechselt wird — das ist
|
|
# ein GANZ ANDERES Ding: Hermes' eigener Gateway-Begriff meint die Messaging-
|
|
# Bruecke (Telegram/Discord/...) + optionalen OpenAI-API-Server, NICHT unseren
|
|
# Auth-Proxy vor Claude).
|
|
#
|
|
# VORAUSSETZUNG: der echte Hermes-Agent-Sourcecode muss danebenliegen, weil
|
|
# das Image nicht auf Docker Hub existiert, sondern per `build:` aus dem Repo
|
|
# gebaut wird. Einmalig VOR dem ersten `docker compose up -d`:
|
|
# git clone https://github.com/NousResearch/hermes-agent.git hermes-agent-src
|
|
#
|
|
# network_mode: host wie im Original — manche Messaging-Plattformen brauchen
|
|
# echte Host-Ports, und dadurch erreicht Hermes unser hermes-gateway einfach
|
|
# ueber "http://localhost:${HERMES_GATEWAY_PORT}/v1" statt ueber Docker-DNS.
|
|
hermes-agent:
|
|
build: ./hermes-agent-src
|
|
image: hermes-agent
|
|
# Erspart den unnoetigen "pull access denied"-Warning-Roundtrip von eben:
|
|
# sagt Compose direkt "nie pullen, immer lokal bauen" statt erst zu
|
|
# pullen und beim Scheitern auf build: zurueckzufallen.
|
|
pull_policy: build
|
|
container_name: hermes-agent
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
depends_on:
|
|
- hermes-gateway
|
|
volumes:
|
|
# ~/.hermes-Aequivalent: Config, Sessions, Memory, Skills - alles persistent
|
|
# hier drin. hermes-agent-config/config.yaml.example wird VOR dem ersten
|
|
# Start einmalig hierher kopiert (siehe README) und dann von Hermes
|
|
# NICHT mehr angefasst (nur geseedet wenn's fehlt).
|
|
- ./hermes-data/agent-home:/opt/data
|
|
environment:
|
|
- HERMES_UID=${HERMES_UID:-10000}
|
|
- HERMES_GID=${HERMES_GID:-10000}
|
|
# Fuer die ${HERMES_GATEWAY_PORT}/${HERMES_GATEWAY_TOKEN}-Platzhalter in
|
|
# hermes-data/agent-home/config.yaml (Hermes expandiert ${VAR} beim
|
|
# Config-Laden aus os.environ — siehe hermes_cli/config.py _expand_env_vars).
|
|
- HERMES_GATEWAY_PORT=${HERMES_GATEWAY_PORT:-8447}
|
|
- HERMES_GATEWAY_TOKEN=${HERMES_GATEWAY_TOKEN:?HERMES_GATEWAY_TOKEN muss in .env gesetzt sein - siehe .env.example}
|
|
# Optional: Hermes' EIGENER OpenAI-kompatibler API-Server, falls Du spaeter
|
|
# einen mobilen Client (gpt_mobile, Maid, OpenWebUI, ...) direkt GEGEN
|
|
# Hermes sprechen lassen willst (andere Richtung als unser Proxy oben!
|
|
# Das hier ist Hermes ALS Server fuer einen Chat-Client, nicht Claude ALS
|
|
# Backend fuer Hermes). Zum Aktivieren beide Zeilen einkommentieren und
|
|
# API_SERVER_KEY in .env setzen:
|
|
# - API_SERVER_HOST=0.0.0.0
|
|
# - API_SERVER_KEY=${API_SERVER_KEY}
|
|
command: ["gateway", "run"]
|
|
|
|
hermes-agent-dashboard:
|
|
# Gleicher Build-Kontext wie oben bei "hermes-agent" (nicht nur "image:").
|
|
# Grund: "hermes-agent" ist kein Docker-Hub-Image, sondern wird lokal aus
|
|
# ./hermes-agent-src gebaut. Ohne eigenes build: hier versucht Compose
|
|
# beim "up" trotzdem erst einen Registry-Pull fuer dieses Image, der mit
|
|
# "pull access denied" fehlschlaegt UND (anders als beim hermes-agent-
|
|
# Service oben, der einen build:-Fallback hat) hart abbricht statt lokal
|
|
# zu bauen. Mit build: hier baut Compose denselben Kontext nochmal --
|
|
# dank Docker-Layer-Cache praktisch instant, landet aber wieder unter
|
|
# dem Tag "hermes-agent".
|
|
build: ./hermes-agent-src
|
|
image: hermes-agent
|
|
pull_policy: build
|
|
container_name: hermes-agent-dashboard
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
depends_on:
|
|
- hermes-agent
|
|
volumes:
|
|
- ./hermes-data/agent-home:/opt/data
|
|
environment:
|
|
- HERMES_UID=${HERMES_UID:-10000}
|
|
- HERMES_GID=${HERMES_GID:-10000}
|
|
# HERMES_DASHBOARD_USER wird hier NICHT fuer Hermes' eigenes Auth-Gate
|
|
# gebraucht (das Gate engagiert sich laut eigener Fehlermeldung nur bei
|
|
# einem 0.0.0.0-Bind, der Service unten bindet aber fest auf 127.0.0.1
|
|
# per `command:`) — steht nur der Vollstaendigkeit halber mit drin,
|
|
# tatsaechlich genutzt wird die Variable von Caddy (siehe unten) fuer
|
|
# dessen Basic-Auth. Absicherung nach aussen laeuft komplett ueber Caddy.
|
|
- HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin}
|
|
# Kurskorrektur 19.07.2026, spaeter Abend: Stefan wollte erst 0.0.0.0
|
|
# (SSH-Tunnel machte mit SSL Probleme), jetzt aber die richtige Loesung:
|
|
# Dashboard bleibt auf 127.0.0.1 (Hermes' eigenes Auth-Gate verlangt dann
|
|
# gar keinen registrierten Provider mehr, siehe Fehlermeldung "the auth
|
|
# gate engages on non-loopback binds") und der Caddy-Reverse-Proxy unten
|
|
# macht TLS + Basic-Auth auf Port 443 nach aussen. Das ist die einzige
|
|
# Auth-Schicht die im Normalbetrieb wirklich noetig ist.
|
|
command: ["dashboard", "--host", "127.0.0.1", "--no-open"]
|
|
|
|
# ─── Caddy (TLS-Reverse-Proxy + Basic-Auth vor dem Dashboard) ──────────
|
|
# Stefans Entscheidung 19.07.2026: Dashboard bleibt intern auf 127.0.0.1,
|
|
# Caddy uebernimmt TLS-Termination auf Port 443 UND die Basic-Auth davor.
|
|
# Caddy ist die EINZIGE Auth-/TLS-Schicht (aufgeraeumt 20.07.2026): Hermes'
|
|
# eigenes Dashboard-Auth-Gate (dashboard.basic_auth) wurde komplett entfernt,
|
|
# weil es sich laut eigener Fehlermeldung ohnehin nur bei einem 0.0.0.0-Bind
|
|
# engagiert — der Dashboard-Container bindet aber fest auf 127.0.0.1, das
|
|
# Gate haette hier nie gegriffen. Ein zweiter, nie greifender Auth-Layer
|
|
# haette nur Setup-Schritte gekostet, kein Sicherheitsgewinn. Nutzt
|
|
# HERMES_DASHBOARD_USER (aus .env) als Login-Name + einen eigenen bcrypt-
|
|
# Hash in CADDY_BASIC_AUTH_HASH (Caddy verlangt bcrypt, siehe .env.example
|
|
# fuer die Erzeugung).
|
|
#
|
|
# Zertifikat: bewusst KEIN Let's Encrypt (bräuchte oeffentliche Domain +
|
|
# Port-80-Challenge). Stefans Wunsch: ein selbstsigniertes Zertifikat mit
|
|
# 100 Jahren Laufzeit, einmalig im Browser als Ausnahme akzeptieren, nie
|
|
# wieder um Erneuerung kuemmern. Erzeugung siehe README Setup-Schritt.
|
|
# network_mode: host aus demselben Grund wie bei hermes-agent/-dashboard:
|
|
# muss 127.0.0.1:9119 (Dashboard) erreichen UND selbst auf dem Host-Port
|
|
# 443 lauschen.
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
container_name: hermes-caddy
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
depends_on:
|
|
- hermes-agent-dashboard
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
# kein :ro mehr -- der Entrypoint unten schreibt das Zertifikat selbst
|
|
# ins Volume, wenn noch keins da ist.
|
|
- ./hermes-data/caddy-certs:/certs
|
|
- ./hermes-data/caddy-data:/data
|
|
- ./hermes-data/caddy-config:/config
|
|
environment:
|
|
- HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin}
|
|
- CADDY_BASIC_AUTH_HASH=${CADDY_BASIC_AUTH_HASH:?CADDY_BASIC_AUTH_HASH muss in .env gesetzt sein - siehe .env.example}
|
|
# Erzeugt beim allerersten Start automatisch ein selbstsigniertes
|
|
# 100-Jahre-Zertifikat im /certs-Volume (persistent, ueberlebt Neustarts),
|
|
# kein manueller openssl-Befehl auf dem Host mehr noetig. apk add ist
|
|
# noetig, weil das caddy:2-alpine-Image openssl nicht mitbringt.
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
set -e
|
|
if [ ! -f /certs/dashboard.crt ] || [ ! -f /certs/dashboard.key ]; then
|
|
echo "[caddy-init] Kein Zertifikat in /certs gefunden -- erzeuge selbstsigniertes 100-Jahre-Zertifikat..."
|
|
apk add --no-cache openssl >/dev/null
|
|
openssl req -x509 -nodes -newkey rsa:4096 \
|
|
-keyout /certs/dashboard.key -out /certs/dashboard.crt \
|
|
-days 36500 -subj "/CN=hermes-dashboard"
|
|
echo "[caddy-init] Zertifikat erzeugt."
|
|
else
|
|
echo "[caddy-init] Zertifikat bereits vorhanden, ueberspringe Erzeugung."
|
|
fi
|
|
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
|
|
networks:
|
|
hermes-net:
|
|
driver: bridge
|