Files
serverAppConfigs/dankclaw/compose.yaml
T
Frank 5e6ba222cb compose: point caddy's /srv/homepage at the host dir, not the repo
The caddy container mounted the repo's shared/homepage (the static source) to
/srv/homepage. But the deploy writes the generated files (status.json from the
status checker, deployed.js from the deploy) to the host's /srv/homepage. So
the container served the static files (200) but 404'd the generated ones.
Mount the host's /srv/homepage instead, which holds both the static files (the
deploy copies them) and the generated ones.
2026-08-23 19:18:41 -07:00

250 lines
9.6 KiB
YAML

# 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
# The homepage: the host's /srv/homepage. The deploy copies the static
# files there; the status checker writes status.json and the deploy
# writes deployed.js there. (Not the repo's shared/homepage, which
# lacks the generated files.)
- /srv/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