# Dankclaw stack — AI, search, development & observability services. # Deploy: cd /home/dank/serverAppConfigs && podman-compose -f dankclaw/compose.yaml up -d # See ../docs/Dankclaw/ and ../docs/Infrastructure/ for service documentation. services: # --- SearXNG stack (gluetun + searxng + valkey) -------------------------- # These three services are now boot-persistent Quadlet units, not compose # services. They live in searxng/quadlet/ and install to # ~/.config/containers/systemd/ on the host. searxng reuses gluetun's netns # (Network=gluetun.container) so its traffic exits the WireGuard tunnel. # Deploy with: scripts/deploy-searxng-quadlet.sh dankclaw # See ../docs/Dankclaw/SearXNG.md, Gluetun.md, Valkey.md. # --- AI image generation (ROCm GPU backend, gfx1151) --------------------- # Built from Containerfile.comfyui on AMD's gfx1151 torch base. The GPU is # reached via the /dev/kfd + /dev/dri passthrough (checked host-side; the run # user is already in the render group). Only the 'video' group is added: # rootless podman cannot map the render gid into the container namespace. # Models + data persist under /home/dank/comfyui-data. See ../docs/Dankclaw/ComfyUI.md. comfyui: build: context: . dockerfile: Containerfile.comfyui image: comfyui-rocm:gfx1151 container_name: comfyui restart: unless-stopped command: ["python", "main.py", "--listen", "0.0.0.0", "--port", "8188"] ports: - "8188:8188" devices: - /dev/kfd:/dev/kfd - /dev/dri:/dev/dri # keep-groups preserves the launching user's real supplementary groups; # a literal `render` entry fails at container start under rootless podman # because the group name cannot be resolved inside the user namespace. group_add: - keep-groups volumes: - /home/dank/comfyui-data/models:/opt/ComfyUI/models - /home/dank/comfyui-data/input:/opt/ComfyUI/input - /home/dank/comfyui-data/output:/opt/ComfyUI/output - /home/dank/comfyui-data/custom_nodes:/opt/ComfyUI/custom_nodes healthcheck: test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8188/')"] interval: 30s timeout: 10s retries: 5 start_period: 180s # --- Chat UI (open-webui) ------------------------------------------------ # Now a boot-persistent Quadlet unit (quadlet, slirp4netns, port 3005), not # a compose service. See open-webui/quadlet/open-webui.container and # ../docs/Dankclaw/OpenWebUI.md. hermes: image: docker.io/nousresearch/hermes-agent:latest container_name: hermes network_mode: host restart: unless-stopped # The image's default s6 entrypoint runs the interactive TUI, which exits # without a TTY and crash-loops the container. Override it to run the # headless gateway (the documented Docker mode). The gateway drops to the # unprivileged hermes user (uid 10000), so the data dir must be writable by it. entrypoint: ["bash", "-c", "hermes gateway run --force -v"] environment: HERMES_DATA_DIR: /data # Point hermes's config/.env at the mounted data dir (persistent). HERMES_HOME: /data # Signal adapter -> the signal-cli daemon, a HOST process listening on # :8080. The service runs with network_mode: host below, so it dials # loopback directly; from a bridge network only DNAT'd published ports # accept connections to the host LAN IP, and un-forwarded ports refuse # instantly (verified live 2026-08-30). SIGNAL_HTTP_URL: "http://127.0.0.1:8080" SIGNAL_ACCOUNT: "${SIGNAL_ACCOUNT}" SIGNAL_ALLOWED_USERS: "${SIGNAL_ALLOWED_USERS}" ports: - "8090:8090" volumes: - /home/dank/hermes-data:/data # No healthcheck: 'hermes gateway run' starts the messaging gateway (Signal # SSE), not the :8090 web dashboard, so there is no HTTP endpoint to probe. # The container's running state + the daemon's SSE connection are the signal. # --- Hermes web dashboard (management UI: config, API keys, sessions) ----- # A separate process from the Signal gateway (the gateway entrypoint does not # start the dashboard). Bound to the LAN with basic auth: the username + a # scrypt password_hash live in the git-ignored /home/dank/hermes-data config. # Port 9119 is published so a browser on the LAN can reach it. hermes-dashboard: image: docker.io/nousresearch/hermes-agent:latest container_name: hermes-dashboard restart: unless-stopped entrypoint: ["bash", "-c", "hermes dashboard --host 0.0.0.0 --port 9119 --no-open --skip-build"] environment: HERMES_DATA_DIR: /data HERMES_HOME: /data ports: - "9119:9119" volumes: - /home/dank/hermes-data:/data # --- Hermes web UI (Hermex server; phone-facing API for the agent) -------- # Runs the open-source hermes-webui (github.com/nesquena/hermes-webui) in # foreground mode: bootstrap.py creates .venv inside the mounted checkout, # installs WebUI + agent deps, then execs server.py as a single foreground # process. It reads HERMES_HOME=/data, so it drives the SAME agent state # (memory, skills, sessions) as the gateway container. # Loopback-only publish: the SSH tunnel (phone -> Dankclaw:22) is the sole # ingress. The password gates Hermex logins and lives in the host .env. hermes-webui: image: docker.io/nousresearch/hermes-agent:latest container_name: hermes-webui restart: unless-stopped entrypoint: ["bash", "-c", "python3 /webui/bootstrap.py --foreground"] environment: HERMES_HOME: /data # Container-internal bind: 0.0.0.0 so Podman's port forwarder (which # dials the container's bridge IP) can reach it. The *host-side* # publish below stays on 127.0.0.1, so loopback-only ingress holds. HERMES_WEBUI_HOST: 0.0.0.0 HERMES_WEBUI_PORT: "8787" HERMES_WEBUI_PASSWORD: "${HERMES_WEBUI_PASSWORD}" # Headless host: never show the first-run wizard (a profile already # exists in /data from the gateway). HERMES_WEBUI_SKIP_ONBOARDING: "1" ports: - "127.0.0.1:8787:8787" volumes: - /home/dank/hermes-data:/data # rw on purpose: bootstrap.py creates /webui/.venv in the repo root. - /home/dank/hermes-webui:/webui healthcheck: test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/health')"] interval: 30s timeout: 10s retries: 5 # First boot pip-installs WebUI + agent deps into a fresh venv. start_period: 600s # --- Git hosting (Gitea) ------------------------------------------------- # Now boot-persistent Quadlet units (private bridge network, DNS enabled), # not compose services. See gitea/quadlet/ and ../docs/Dankclaw/Gitea.md. # --- Observability ------------------------------------------------------- # Now boot-persistent Quadlet units, not compose services: prometheus, # grafana, loki, alloy and node-exporter install from # dankclaw/observability/quadlet/ and are dispatched by deploy_observability # in scripts/deploy-remote.sh. They left compose on 2026-09-11, after a host # reboot proved that compose-only containers do not come back on their own — # the same gap that had already cost the fleet Caddy and Pi-hole. # # The three HTTP UIs publish on 127.0.0.1 only. Off-host access is through # the guarded franksdank.cloud vhosts (Caddy reaches these containers by # name over this network) and the host's own homepage status timer probes # them over loopback. See # serverAppConfigs-docs/Infrastructure/Observability.md. # --- Authentik (SSO / OIDC provider) ------------------------------------- # Now boot-persistent Quadlet units, not compose services: authentik- # postgres -> authentik-server -> authentik-worker start in dependency order # via ~/.config/containers/systemd/*.container on the host (repo source: # dankclaw/authentik/quadlet/; deploy-remote.sh deploy_authentik installs # and starts them). Redis was removed in 2025.10 (sessions/caching moved to # PostgreSQL). The Caddyfile's forward_auth vhosts proxy to # authentik-server:9000 over the dankclaw_default network (A2A DNS). # See ../docs/Infrastructure/Authentik.md. # Caddy (the homelab's single reverse proxy) left compose on 2026-08-26: # it is boot-persistent via its Quadlet unit (dankclaw/caddy/quadlet/ # caddy.container), installed by deploy_caddy in scripts/deploy-remote.sh. # The rendered Caddyfile, certs, and config still live under # /home/dank/Containers/caddy (rootless-writable).