# ============================================================================= # services.yml — THE service inventory. # # One entry per deployable service, in canonical deploy order (dependencies # before consumers, the LLM fleet last). Four hand-maintained lists used to # agree with each other by discipline alone; every one of them now READS this # file: # # scripts/deploy-plan.py which services a changed path selects # scripts/deploy-remote.sh which services a target accepts (allow-list) # scripts/test-changed.sh which Bats seams a changed path must re-run # scripts/service-map.py renderers + drift gates (`check`) # # `service-map.py check` is the referee: every quadlet unit file and every # root unit in the tree is claimed by exactly one entry, every repo file is # owned by a service or listed in `unowned:` with a reason, every seam exists, # the rendered Pi-hole hosts file equals the render, and Prometheus' scrape # targets plus the homepage probes equal the `scrape:` / `probe:` facts. # # --- fields ------------------------------------------------------------------ # name the deployer's service word == the systemd unit name for # quadlet / root-unit / vm-quadlet kinds # hosts targets that accept it (the deploy-remote.sh allow-list) # kind quadlet rootless user unit from a .container file # root-unit systemd system unit symlinked out of scripts/ # vm-quadlet quadlet inside the FrankBook-Pro podman machine # host a host-level script/timer, no container # deployed_by this service deploys as part of another deployable's handler # (the trio/pair shape). It has no dirs/files of its own; the # owner names the deployable whose handler installs it. # unit_dir repo dir holding .container — a scalar, or a per-host map # when one service carries its unit on two hosts (node-exporter) # units container names this deployable owns (netns coupling is derived # from the `Network=` lines in those files, never declared here) # dirs repo dirs whose every file selects this service # files exact repo files that select this service (root-unit trios, # vendored templates, host scripts installed by this handler) # retires files whose DELETION selects this service (the unit the # handler must remove before it can claim the ports) # only Dankserver's media stack still has one) # health the deployer's readiness URL; `wait` its 2 s poll budget # scrape Prometheus jobs: {job, target, metrics_path?, labels?} # dns names this service answers at (rendered into the Pi-hole # addn-hosts; every vhost resolves at the Dankclaw Caddy edge) # probe host:port the homepage checker dials (per-host map if on 2 hosts) # tests the Bats seams a change to this service must re-run # ============================================================================= hosts: dankclaw: address: 192.168.0.143 # Caddy is consolidated on Dankclaw: it fronts every service on every # host, so every DNS name in the fleet resolves here. dns_edge: 192.168.0.143 dankserver: address: 192.168.0.235 frankbook-pro: address: 192.168.0.112 # Repo files no service owns. Each entry is a fact about the tree, not a # fallback: the census gate fails on anything unowned that is NOT listed, so # retirement work shrinks this list and a new stray file cannot slip in. unowned: shared/services.yml: the inventory this tool reads scripts: repo tooling (build, fetch, bench, deploy, test); the deployers' files are claimed by name in the service entries autoresearch.sh: bench scratch at the repo root (retirement pending) echo_prompt.txt: bench fixture at the repo root (retirement pending) echo_request.json: bench fixture at the repo root (retirement pending) prose_prompt.txt: bench fixture at the repo root (retirement pending) prose_request.json: bench fixture at the repo root (retirement pending) dankclaw/apply_qwen21_patch.py: retired patch script (its target runtime is gone) dankclaw/comfyui-start.sh: superseded by the comfyui quadlet + Containerfile dankclaw/udev: host udev rules, applied by manual sudo install + udevadm trigger tests: the seams themselves report: frozen point-in-time audit reports old: retired scripts, reference only .githooks: the commit hooks that run the seams services: # --- Search stack: one netns family, gluetun owns it --------------------- - name: gluetun hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/searxng/quadlet units: [gluetun] tests: [tests/searxng_quadlet.bats] # The tunnel's WireGuard bind — the homepage card for the tunnel itself. probe: 127.0.0.1:51820 - name: searxng-valkey hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/searxng/quadlet units: [searxng-valkey] # The app-level law, not a network one: deploy_searxng_valkey restarts # SearXNG after its cache, so the cache's files select both. dependents: [searxng] tests: [tests/searxng_quadlet.bats] probe: 127.0.0.1:6379 - name: searxng hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/searxng/quadlet units: [searxng] dirs: [shared/searxng] tests: [tests/searxng_limiter.bats, tests/searxng_quadlet.bats] scrape: - job: services target: 192.168.0.143:8081 metrics_path: / dns: [search.franksdank.cloud] probe: 127.0.0.1:8081 - name: marreta hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/marreta/quadlet units: [marreta, marreta-browser] tests: [tests/marreta_quadlet.bats] dns: [marreta.franksdank.cloud] probe: 127.0.0.1:8082 # --- DNS ------------------------------------------------------------------ - name: pihole hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/pihole/quadlet units: [pihole] dirs: [dankclaw/pihole] tests: [tests/pihole.bats, tests/docs_completeness.bats] dns: [pihole.franksdank.cloud] probe: 192.168.0.143:8083 # --- Chat front end ------------------------------------------------------- - name: open-webui hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/open-webui/quadlet units: [open-webui] tests: [tests/open_webui_quadlet.bats, tests/caddyfile.bats] scrape: - job: services target: 192.168.0.143:3005 metrics_path: / dns: [ai.franksdank.cloud] probe: 127.0.0.1:3005 # --- Git hosting ---------------------------------------------------------- - name: gitea-postgres hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/gitea/quadlet units: [gitea-postgres] deployed_by: gitea-server tests: [tests/caddyfile.bats, tests/docs_completeness.bats] probe: 127.0.0.1:5432 - name: gitea-server hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/gitea/quadlet units: [gitea-server] tests: [tests/caddyfile.bats, tests/docs_completeness.bats] scrape: - job: gitea target: 192.168.0.143:3000 metrics_path: /metrics dns: [gitea.franksdank.cloud] probe: 127.0.0.1:3000 # --- Image generation ----------------------------------------------------- - name: comfyui hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/comfyui/quadlet units: [comfyui] dirs: [dankclaw/comfyui] files: [dankclaw/Containerfile.comfyui] health: http://127.0.0.1:8188/ wait: 300 tests: [tests/comfyui_rocm.bats, tests/docs_completeness.bats] scrape: - job: comfyui target: comfyui:8188 metrics_path: / dns: [comfyui.franksdank.cloud] probe: 127.0.0.1:8188 # --- Requests ------------------------------------------------------------- - name: seerr hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/seerr/quadlet units: [seerr] tests: [tests/seerr_quadlet.bats, tests/deployment_flow.bats] dns: [seerr.franksdank.cloud] probe: 127.0.0.1:5055 # --- Agent stack: one handler, four containers ---------------------------- - name: hermes hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/hermes/quadlet units: [hermes, hermes-dashboard, hermes-webui, chrome-devtools-protocol] dirs: [dankclaw/hermes] tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] dns: [hermes.franksdank.cloud] probe: 127.0.0.1:8787 - name: hermes-dashboard hosts: [dankclaw] kind: quadlet deployed_by: hermes tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] dns: [hermesadmin.franksdank.cloud] probe: 192.168.0.143:9119 - name: hermes-webui hosts: [dankclaw] kind: quadlet deployed_by: hermes tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] # --- Observability -------------------------------------------------------- - name: prometheus hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/observability/quadlet units: [prometheus] dirs: [shared/prometheus] tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] scrape: - job: prometheus target: localhost:9090 dns: [prometheus.franksdank.cloud] probe: 127.0.0.1:9090 - name: grafana hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/observability/quadlet units: [grafana] dirs: [shared/grafana] tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] scrape: - job: grafana target: grafana:3000 metrics_path: /metrics dns: [grafana.franksdank.cloud] probe: 127.0.0.1:3002 - name: loki hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/observability/quadlet units: [loki] dirs: [dankclaw/loki, shared/loki] tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] scrape: - job: loki target: loki:3100 metrics_path: /metrics dns: [loki.franksdank.cloud] probe: 127.0.0.1:3100 - name: alloy hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/observability/quadlet units: [alloy] dirs: [dankclaw/alloy, shared/alloy] tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] probe: 127.0.0.1:12345 - name: node-exporter # One service on two hosts: the same exporter, its own unit per host. hosts: [dankclaw, dankserver] kind: quadlet unit_dir: dankclaw: dankclaw/observability/quadlet dankserver: dankserver/observability/quadlet units: [node-exporter] tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] scrape: - job: node-dankclaw target: host.containers.internal:9100 - job: node-dankserver target: 192.168.0.235:9100 probe: 127.0.0.1:9100 - name: alertmanager hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/observability/quadlet units: [alertmanager] # The Alertmanager config ships from Prometheus' directory, and the Signal # relay is a HOST unit this handler installs: a change to either reaches # the machine only through an alertmanager deploy. files: - shared/prometheus/alertmanager.yml - scripts/signal-relay.py - scripts/signal-relay.service tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, tests/docs_completeness.bats] probe: 127.0.0.1:9093 # --- Identity ------------------------------------------------------------- - name: authentik-postgres hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/authentik/quadlet units: [authentik-postgres] deployed_by: authentik-server tests: [tests/authentik.bats] - name: authentik-server hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/authentik/quadlet units: [authentik-server] tests: [tests/authentik.bats] dns: [auth.franksdank.cloud] - name: authentik-worker hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/authentik/quadlet units: [authentik-worker] deployed_by: authentik-server tests: [tests/authentik.bats] - name: authentik-outpost hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/authentik/quadlet units: [authentik-outpost] tests: [tests/authentik.bats] - name: authentik-outpost-ldap hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/authentik/quadlet units: [authentik-outpost-ldap] tests: [tests/authentik.bats] # --- The auth chain's edge, then the edge itself -------------------------- - name: anubis hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/anubis/quadlet units: [anubis] dirs: [dankclaw/anubis] tests: [tests/anubis.bats, tests/caddyfile.bats, tests/homepage_caddy.bats] - name: caddy hosts: [dankclaw] kind: quadlet unit_dir: dankclaw/caddy/quadlet units: [caddy] dirs: [dankclaw/caddy] files: [dankclaw/Caddyfile] # Retiring the old split proxy selects Caddy: it must remove the installed # unit and container before it can claim 80/443. retires: [dankclaw/gitea/quadlet/gitea-caddy.container] tests: [tests/caddy_quadlet.bats, tests/caddyfile.bats, tests/homepage_caddy.bats, tests/docs_completeness.bats] dns: - franksdank.cloud - www.franksdank.cloud - dankclaw.franksdank.cloud - dankserver.franksdank.cloud probe: 127.0.0.1:80 - name: homepage hosts: [dankclaw] kind: host dirs: [shared/homepage] files: - scripts/homepage-status.py - scripts/homepage-status.service - scripts/homepage-status.timer tests: [tests/homepage_caddy.bats, tests/homepage_config.bats, tests/homepage_deploy.bats, tests/homepage_pages.bats, tests/homepage_checker.bats] # --- Dankserver media stack ---------------------------------------------- # The romm trio is one handler and one directory: every file in it selects # all three, exactly as the deployer restarts all three. - name: romm-mariadb hosts: [dankserver] kind: quadlet unit_dir: dankserver/romm/quadlet units: [romm-mariadb] deployed_by: romm dirs: [dankserver/romm] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] probe: 192.168.0.235:3306 - name: romm-redis hosts: [dankserver] kind: quadlet unit_dir: dankserver/romm/quadlet units: [romm-redis] deployed_by: romm dirs: [dankserver/romm] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] - name: romm hosts: [dankserver] kind: quadlet unit_dir: dankserver/romm/quadlet units: [romm] dirs: [dankserver/romm] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] dns: [romm.franksdank.cloud] probe: 192.168.0.235:8085 - name: copyparty hosts: [dankserver] kind: quadlet unit_dir: dankserver/copyparty/quadlet units: [copyparty] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] dns: [files.franksdank.cloud] probe: 192.168.0.235:3923 - name: jellyfin hosts: [dankserver] kind: quadlet unit_dir: dankserver/jellyfin/quadlet units: [jellyfin] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] dns: [jelly.franksdank.cloud] probe: 192.168.0.235:8096 - name: calibre-web hosts: [dankserver] kind: quadlet unit_dir: dankserver/calibre-web/quadlet units: [calibre-web] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] dns: [books.franksdank.cloud] probe: 192.168.0.235:8083 - name: calibre-server hosts: [dankserver] kind: quadlet unit_dir: dankserver/calibre-server/quadlet units: [calibre-server] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] - name: plex hosts: [dankserver] kind: quadlet unit_dir: dankserver/plex/quadlet units: [plex] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] dns: [plex.franksdank.cloud, plex.franksdank.casa] probe: 192.168.0.235:32400 - name: tautulli hosts: [dankserver] kind: quadlet unit_dir: dankserver/tautulli/quadlet units: [tautulli] tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] probe: 192.168.0.235:8181 # --- FrankBook-Pro: the *arr stack inside the podman machine -------------- # Every container rides gluetun's netns, and that coupling is DERIVED: each # arr's unit file says `Network=gluetun.container`, so a gluetun unit change # selects the whole stack with no declaration here. - name: gluetun hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/gluetun/quadlet units: [gluetun] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] - name: prowlarr hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/prowlarr/quadlet units: [prowlarr] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] dns: [prowlarr.franksdank.cloud] probe: 192.168.0.112:9696 - name: qbittorrent hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/qbittorrent/quadlet units: [qbittorrent] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] dns: [qbittorrent.franksdank.cloud] probe: 192.168.0.112:8080 - name: radarr hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/radarr/quadlet units: [radarr] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] dns: [radarr.franksdank.cloud] probe: 192.168.0.112:7878 - name: sonarr hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/sonarr/quadlet units: [sonarr] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] probe: 192.168.0.112:8989 - name: lidarr hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/lidarr/quadlet units: [lidarr] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] dns: [lidarr.franksdank.cloud] probe: 192.168.0.112:8686 - name: readarr hosts: [frankbook-pro] kind: vm-quadlet unit_dir: frankbook-pro/readarr/quadlet units: [readarr] tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] dns: [readarr.franksdank.cloud] probe: 192.168.0.112:8787 # --- The LLM fleet (last: never implied by a generic change) -------------- - name: frank-dragoon hosts: [dankclaw] kind: root-unit files: - scripts/frank-dragoon.env - scripts/frank-dragoon.service - scripts/serve-frank-dragoon.sh health: http://127.0.0.1:7770/health wait: 1200 tests: [tests/frank_dragoon_unit.bats, tests/llm_units.bats, tests/llm_deploy_gates.bats, tests/llm_secrets.bats, tests/scripts_syntax.bats, tests/docs_completeness.bats] scrape: - job: frank-dragoon target: host.containers.internal:7770 metrics_path: /metrics labels: {service: frank-dragoon} probe: 127.0.0.1:7770 - name: frank-zealot hosts: [dankclaw] kind: root-unit files: - scripts/frank-zealot.env - scripts/frank-zealot.service - scripts/serve-frank-zealot.sh health: http://127.0.0.1:7771/health wait: 900 tests: [tests/frank_zealot_unit.bats, tests/llm_units.bats, tests/llm_deploy_gates.bats, tests/llm_secrets.bats, tests/scripts_syntax.bats] scrape: - job: frank-zealot target: host.containers.internal:7771 metrics_path: /metrics labels: {service: frank-zealot} probe: 127.0.0.1:7771 - name: frank-archon hosts: [dankserver] kind: root-unit files: - scripts/frank-archon.env - scripts/frank-archon.service - scripts/serve-frank-archon.sh # The vendored chat template the engine renders with. - scripts/qwen38-froggeric-v22.5.jinja health: http://127.0.0.1:7770/health wait: 900 tests: [tests/frank_archon_unit.bats, tests/llm_units.bats, tests/llm_deploy_gates.bats, tests/llm_secrets.bats, tests/scripts_syntax.bats] scrape: - job: frank-archon target: 192.168.0.235:7770 metrics_path: /metrics labels: {service: frank-archon} probe: 192.168.0.235:7770