# 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 group_add: - video 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 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 host signal-cli daemon. The slirp host alias # (host.containers.internal) can drop at runtime; the host LAN IP is the # stable container->host path. 127.0.0.1 inside the container is not the host. SIGNAL_HTTP_URL: "http://192.168.0.143: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 # --- 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 ------------------------------------------------------- prometheus: image: docker.io/prom/prometheus:latest container_name: prometheus restart: unless-stopped # Rootless: container root maps to the invoking user (dank), which # owns /opt/prometheus/data. The image's default user (nobody) maps to # a subuid that cannot write it. user: "0:0" command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=30d - --web.enable-lifecycle ports: - "9090:9090" volumes: - /opt/prometheus/config:/etc/prometheus - /opt/prometheus/data:/prometheus healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy"] interval: 30s timeout: 10s retries: 3 grafana: image: docker.io/grafana/grafana:latest container_name: grafana restart: unless-stopped # Rootless: container root maps to the invoking user (dank), which # owns /opt/grafana/data. The image's default user (472) maps to a # subuid that cannot write it. user: "0:0" environment: GF_SERVER_ROOT_URL: "https://grafana.franksdank.cloud" GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}" GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD}" ports: - "3002:3000" volumes: - /opt/grafana/data:/var/lib/grafana - /opt/grafana/provisioning:/etc/grafana/provisioning:ro depends_on: - prometheus healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 3 loki: # Local build: grafana/loki + static busybox (healthcheck). image: franksdank/loki:latest container_name: loki restart: unless-stopped # Rootless: container root maps to the invoking user (dank), which # owns /opt/loki/data. The image's default user (10001) maps to a # subuid that cannot write it. user: "0:0" command: ["-config.file=/etc/loki/loki-config.yaml"] ports: - "3100:3100" volumes: - /opt/loki/config:/etc/loki - /opt/loki/data:/loki healthcheck: test: ["CMD", "/usr/local/bin/busybox", "wget", "--spider", "-q", "http://localhost:3100/ready"] interval: 30s timeout: 10s retries: 3 alloy: # Local build: grafana/alloy + static busybox (healthcheck). image: franksdank/alloy:latest container_name: alloy restart: unless-stopped volumes: - /opt/alloy/config:/etc/alloy - /run/systemd/journal/socket:/run/systemd/journal/socket:ro - /var/log:/var/log:ro - /var/lib/containers:/var/lib/containers:ro depends_on: - loki healthcheck: test: ["CMD", "/usr/local/bin/busybox", "wget", "--spider", "-q", "http://localhost:12345/-/healthy"] interval: 30s timeout: 10s retries: 3 node-exporter: image: docker.io/prom/node-exporter:latest container_name: node-exporter restart: unless-stopped network_mode: host pid: host volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - --path.procfs=/host/proc - --path.sysfs=/host/sys - --path.rootfs=/rootfs healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:9100/metrics"] interval: 30s timeout: 10s retries: 3 # --- Reverse proxy + homepage (the homelab's single Caddy) --------------- # The Caddyfile mounted here is the RENDERED copy (basicauth hash filled in # from the host .env by scripts/deploy.sh); the repo copy carries the # REPLACE_WITH_HASH placeholder. Certs/config live under # /home/dank/Containers/caddy (rootless-writable). caddy: # Locally built: stock Caddy + the Porkbun DNS provider # (dankclaw/caddy/Containerfile). See that file for build instructions. image: franksdank/caddy-porkbun:latest container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" environment: # Consumed by the Caddyfile's acme_dns block (DNS-01 wildcard certs). PORKBUN_API_KEY: ${PORKBUN_API_KEY} PORKBUN_API_SECRET_KEY: ${PORKBUN_API_SECRET_KEY} volumes: - /home/dank/Containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - /home/dank/Containers/caddy/data:/data - /home/dank/Containers/caddy/config:/config - ../shared/homepage:/srv/homepage:ro depends_on: - comfyui - hermes - grafana - prometheus - loki healthcheck: # The bare-IP catch-all answers 302 (curl -f passes on <400); this # proves Caddy itself is serving without touching any upstream. test: ["CMD", "curl", "-sf", "-o", "/dev/null", "http://localhost:80/"] interval: 30s timeout: 10s retries: 3