Commit Graph
30 Commits
Author SHA1 Message Date
Frank 4183e31fd1 frank-medium: default RADIANCE_NORMQUANT_FUSION under set -u 2026-09-15 21:02:50 -07:00
Frank 6b647c1558 frank-medium: swap ROCmFPX llama.cpp runtime for vllm-radiance MXFP4
Rootless Quadlet on dankserver serving
just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq (the radiance stack's
designated alternate target; ships its fp8 MTP head, no rewrite step) with
the DFlash2-FP8 drafter at SPEC=7 on the 2x R9700s (TP=2, host network,
:7770, alias frank-medium unchanged).

- dankserver/frank-medium/: quadlet unit, container entrypoint (fork of
  serve-mxfp4.sh's patch block at pinned clone commit 97353293), healthcheck
- dispatcher retires the root llama.cpp unit, re-tags the registry image to
  the localhost alias, installs the quadlet, waits on /health
- planner maps dankserver/frank-medium/* to frank-medium; scripts-era
  mappings removed
- ROCmFPX-era env/serve/unit/download scripts staged in scripts/.AGENTTRASH
  with .rocmfpx suffix; seams updated (frank_medium_quadlet.bats new)
2026-09-15 20:59:55 -07:00
Frank 4c7465d00d arr fleet: FrankBook-Pro is the sole deployment, all egress VPN-gated
FrankBook-Pro becomes the only *arr/qBittorrent deployment. Every fleet
container (qB + five arrs) now shares gluetun's netns, so all egress —
torrent, indexer APIs, metadata lookups — exits the WireGuard tunnel by
construction (AllowedIPs=0.0.0.0/0); the arrs' own ports publish on the
gluetun unit, none on the arr units themselves.

- dankserver/compose.yaml: remove the six arr services (the Aug 20 compose
  copy — never healthy, superseded; config dirs trashed on the host)
- dankclaw/Caddyfile + shared/homepage/services.json: arr vhosts and
  probes repoint 192.168.0.235 -> 192.168.0.112 (FrankBook-Pro)
- frankbook-pro units: five arrs join gluetun netns (Wants/After/Network);
  gluetun publishes 9696/8989/7878/8686/8787 alongside 8080/6881
- tests/frankbook_quadlet.bats: new VPN egress seam — every arr+qB unit
  must share gluetun's netns and publish no port; gluetun must publish the
  whole fleet's ports
- deploy-plan.py: gluetun changes carry the whole netns family (a gluetun
  recreation kills all six joiners); dankserver compose set loses the arrs
- AGENTS.md: deployment is complete only when unit tests AND the Playwright
  E2E suite both pass
2026-09-04 12:54:28 -07:00
Frank e4541598ff revert(dankserver): drop userns_mode from arr services
Rootless podman on dankserver already maps container root (PUID=0) to
the dank user (uid 1000) by default — /proc/<pid>/uid_map reads
0->1000, 1..->100000+. Verified across plex, tautulli, jellyfin,
calibre-web, copyparty, romm. userns_mode: host is unnecessary, and
podman-compose 1.5.0 cannot apply it anyway (compose services run in
pods; --userns and --pod cannot be set together). The real fix for the
never-booted arr stack was chowning the root-owned /opt/<svc>/config
dirs to 1000:1000.
2026-09-04 12:26:16 -07:00
Frank df10a33ead fix(dankserver): arr stack runs with userns_mode host — stack never booted rootless
The arr services were committed (b9de886) with PUID=0 but no userns_mode.
Under rootless podman's default subuid mapping, container-root cannot write
host-root-owned bind mounts, so every app died in logger init
(UnauthorizedAccessException on /config/Sentry) before binding its port —
the stack 502'd since it was first brought up (2026-09-04 00:33).

Per the fleet convention (AGENTS.md, plex/tautulli migration): PUID=0 +
UserNS=host maps container-root to dank (1000), which owns /opt data roots
and /mnt/entropy. Config dirs chowned 1000:1000 at bring-up.

Also drops a duplicate 'networks: [appnet]' key on prowlarr.
2026-09-04 12:14:09 -07:00
Frank 8895a02512 dankserver: entropy read-only fleet-wide; jellyfin gets whole /mnt/entropy at /data 2026-09-01 23:03:45 -07:00
Frank 6302508dfe plex: restore Network=dankserver_appnet:alias=plex (line churn ate it)
The C5 probe failed for the wrong reason: plex.container lost its Network=
key in the same edit churn that ate tautulli's ContainerName. Plex ran on
the default podman network — published 32400 still worked (clients fine)
but tautulli could not resolve the plex alias, breaking the seeded
PMS_Web_Host=plex. Restore appnet membership; structure now parallel to
tautulli.container. Lesson recorded: after multi-hunk line edits on these
unit files, diff the whole file — key-order churn has eaten three keys
already.
2026-09-01 23:02:31 -07:00
Frank 39c8135a36 tautulli: restore ContainerName=tautulli (line churn ate it)
Without ContainerName= the v1 generator names the container
systemd-tautulli; the fleet references containers by their plain names
(podman inspect/exec, deployer health checks). Container was healthy but
renamed — caught by post-cutover verification, not by the seam; the seam
only asserts file keys, so pin the name here and keep plex/tautulli
structures parallel.
2026-09-01 22:58:44 -07:00
Frank c694053950 tautulli healthcheck via curl; restore PublishPort; plex gate to 600s
- tautulli.container: HealthCmd switches to the repo's proven curl pattern
  (jellyfin unit) — quadlet's systemd-style value parsing mangles embedded
  double quotes; PublishPort=8181:8181 restored (a mis-aimed line edit
  replaced it; the PublishPort seam test caught the regression)
- deploy_plex readiness gate: 36x5s -> 120x5s; the 2026-09 cutover showed
  the first container boot of a years-old library runs ~10 min while PMS
  pre-binds 32400 and empty-replies; 180s failed the gate twice with a
  healthy, still-booting service
2026-09-01 22:56:19 -07:00
Frank 58aa084d9c quadlet: drop the userns identity model — rootless cannot map uid 997
Evidence from the failed first cutover: podman top shows container root
maps to dank (1000) under UserNS=host rootless (subuid range 100000:65536
cannot express 997), so PUID=997 fell back to root and PMS crash-looped on
write access to its 997:984-owned data root. Fleet model instead: PUID=0/
PGID=0 (container root = dank, same as jellyfin/copyparty data roots) and
deploy_plex chowns /var/lib/plexmediaserver to 1000:1000 inside the
first-cutover block. Zero bytes moved; rollback regains one chown -R
997:984 step, documented in Plex.md. Seam test pins PUID=0/PGID=0 and the
absence of any userns key.
2026-09-01 22:46:00 -07:00
Frank 1939135e4c quadlet: UsernsMode= is compose syntax — use the v1 key UserNS=host
podman 5.7 podman-user-generator on dankserver hard-fails the whole unit on
the unknown key ('unsupported key UsernsMode in group Container'), so the
first cutover attempt masked the native unit and then found no plex.service
at all. Swap both units to UserNS=host (same --userns=host flag), update the
ownership-contract seam test to pin the exact key, and correct both docs.
Pin the seam: the v1 generator rejects unknown Container keys silently at
deploy distance — the test now pins UserNS=, not the misspelled alias.
2026-09-01 22:36:43 -07:00
Frank eebf2f6c7d dankserver: add plex and tautulli quadlet units
- plex: lscr.io/linuxserver/plex:1.43.3.10896-cb3ebc72d-ls322 (verified ==
  ghcr latest digest; plan's ls9 pin superseded by the re-verify clause),
  /var/lib/plexmediaserver bind-mounted in place as /config, /mnt/entropy
  mounted ro at the same path, PUID=997/PGID=984 + UsernsMode=host so the
  container's file identity is byte-compatible with the native unit
- tautulli: lscr.io/linuxserver/tautulli:v2.18.1-ls242 (verified == ghcr
  latest digest), fresh /home/dank/Containers/tautulli config root,
  PUID=1000/PGID=1000, python3 /home healthcheck
- both on dankserver_appnet with aliases; 32400/8181 published as before
2026-09-01 22:31:26 -07:00
Frank a14a70a404 dankserver: copyparty PublishPort=3923 + seam pinning host-served ports
The cutover unit joined dankserver_appnet but dropped the compose-era
0.0.0.0:3923 mapping (live inspect: bridge, 3923 published), so
copyparty listened inside the netns while the host refused. New seam
asserts every host-facing port matches the pre-cutover PortBindings:
romm 8085->8080, jellyfin 8096, calibre-web 8083, copyparty 3923.
2026-09-01 13:53:10 -07:00
Frank 06de2b4cbf dankserver: jellyfin GPU device via AddDevice=, seam guard for Device=
The podman 5.x v1 quadlet generator rejected the whole jellyfin unit on
the unsupported Device= key (generator log: unsupported key 'Device' in
group 'Container'), so no jellyfin.service was generated and the cutover
restart failed with 'Unit not found' after the data root had already
migrated cleanly. gluetun proves AddDevice= is the house key. New seam
test denies Device= across all six dankserver units and pins the
AddDevice=/dev/dri replacement.
2026-09-01 13:49:45 -07:00
Frank b206f477ad dankserver: quadlet units + deploy handlers for romm trio, jellyfin, calibre-web, copyparty
Cutover of the six surviving dankserver services from compose-era bare
containers to boot-persistent Quadlet units (WantedBy=default.target; podman
5.x v1 generator honors it at user-manager start).

Units are faithful to live inspect ground truth (image pins = running tags,
not compose inventory):
- romm trio shares one data root: /home/dank/Containers/romm mounted whole at
  /romm; mariadb datadir and redis data dir live inside it. Aliases mariadb/
  redis preserved on dankserver_appnet via Network=...:alias=. Secrets stay in
  the host-local env file (EnvironmentFile + ${VAR} expansion in the generated
  ExecStart, gitea precedent). copyparty passes its admin account via
  Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} (Exec= not Command=: the v1
  generator hard-fails on Command=); entropy stays rw only there.
- jellyfin keeps every creation-time env as Environment= lines (zero drift);
  Device=/dev/dri + GroupAdd=keep-groups preserve hardware transcoding.
- calibre-web pins the exact running image tag (id 986277563e76 =
  0.6.27-ls397), pasta network preserved.

Deployer: first-cutover handlers retire the old container, rescue unshadowed
anonymous-volume content (podman volume export + tar --skip-old-files), mv
data roots on the same fs, then start units in dependency order with health
gates. Re-deploys restart only the selected unit. deploy-plan.py maps compose
keys mariadb/redis to the new unit names; allowlists updated. New seam test
tests/dankserver_quadlet.bats pins boot-persistence + v1-generator contract;
test-changed.sh maps the new dirs to it. Full suite green (209 tests).
2026-09-01 04:14:08 -07:00
Frank 8e1b00646f copyparty: read-only volume (no uploads); correct runbook anon-perms claim
-v /w::A granted anonymous full rwmda on the LAN (verified: anon PUT -> 201,
PROPFIND -> 207). New perms ::r:ra,dank: browse/download for anonymous
(required by the guarded files.franksdank.cloud chain), admin for dank,
no w/m/d for anyone. Runbook corrected with the --help-accounts semantics.
2026-08-31 16:37:38 -07:00
Frank b9de886509 fix(dankserver): run arr stack and qb as container-root (PUID=0)
Under rootless podman, LSIO's PUID=1000 maps to host uid 101000 — a phantom
user owning nothing: Sonarr's own validator rejected /tv because the process
could not write it. Container-root under a single-user userns maps to the host
user (dank), which owns every /mnt/entropy media dir and /opt config dir.
Identity is now mapping-independent instead of depending on subuid offsets.
calibre-web untouched (working; churn avoided).
2026-08-29 07:18:50 -07:00
Frank 2c8e65e82f fix(dankserver): copyparty ac edition takes accounts and volumes as argv flags
Ground truth from the image itself: python3 -m copyparty --help-accounts shows
-a takes USER:PASS and -v takes SRC:DST:PERMS:FLAGS — the COPAY_PASS/COPAY_MODE
envs this compose carried are not consumed by this image at all (zero COPAY_
references in the package). With no accounts and no volumes configured, the ac
edition serves its cwd read/write for everyone: the stack was running an
unauthenticated world-writable share of the 91T array. Now: admin account dank
with the A flag on volume /w; anonymous gets nothing.
2026-08-29 03:32:18 -07:00
Frank f3ebe0943a revert(dankserver): drop copyparty command override
The ac edition's entrypoint (cpp.sh -> python3 -m copyparty) consumes config
via /z/initcfg, not argv; -v is the volume flag and ate the next arg,
crash-looping argparse. The image mounts everything under /cfg/copyparty as
volumes by marker-file convention — plant the marker on the host instead.
2026-08-29 03:26:46 -07:00
Frank 9cbd8b8822 fix(dankserver): copyparty ac edition takes volumes as command args
The image entrypoint is /bin/ash /z/cpp.sh -c /z/initcfg: copyparty flags are
passed through command, and the ac edition mounts volumes with -m, not via a
COPAY_VOL env. Without any volume mounted every request resolved zero volumes
and the app answered HTTP 500 with its own error page. Verbose flag included
for the bring-up; drop it once stable.
2026-08-29 03:19:41 -07:00
Frank c70d203ad9 fix(dankserver): copyparty volume root via COPAY_VOL
The ac edition maps volumes from COPAY_* envs; without a volume root every
request resolves zero volumes and the app answers HTTP 500 with its own error
page. Point it at /w where the LUKS array is mounted.
2026-08-29 03:17:02 -07:00
Frank 03fd9e244d fix(dankserver): copyparty serves /w from the array; image is the ac edition
ghcr.io/9001/copyparty:latest does not exist — ghcr answers 403 on the token
request for nonexistent repos, which is why the bring-up failed at pull. The
upstream publishes ghcr.io/9001/copyparty-ac (ac = full edition) and serves
whatever is mounted at /w, so the compose mounts the LUKS array there; config
persists in /opt/copyparty/cfg per upstream layout.
2026-08-29 03:09:27 -07:00
Frank d74ea5bf39 fix(dankserver): jellyfin user-mapping and readarr image tag
Jellyfin ran as user 1000:1000 inside a rootless user namespace, where host
uid 1000 maps to container uid 0 — the pinned uid is unmapped, so /config/log
creation EACCES'd and the service SIGSEGV'd on boot. Container root (= host
dank) is correct under rootless podman; group_add 992 (render) still maps via
the userns group map, so GPU access is preserved.

Readarr: lscr.io/linuxserver/readarr:latest does not exist — LSIO ships only
arch-prefixed nightly/version tags for readarr (verified against the ghcr tag
API). Pin amd64-1.0.980, the newest stable tag.
2026-08-29 03:03:55 -07:00
Frank 67de5cfe9b fix(dankserver): point media mounts at the real entropy array paths
The compose stack mounted /mnt/media/{movies,shows,tv,music,books,downloads}
— a prefix that has never existed on this host. The 91T LUKS array mounts at
/mnt/entropy and its libraries are case-exact (Movies, TVShows, Music,
eBooks). Also restores prowlarr's config mount (eaten by an off-by-one in the
prior edit) and fixes TZ New_York->Los_Angeles on the arr stack.
2026-08-29 02:54:24 -07:00
Frank 4a679fce80 Caddy consolidation: single Caddy on dankclaw fronts both hosts
Per user direction, Caddy runs only on dankclaw. It fronts every
service on both hosts: local services by container name, quadlet
services by published LAN port, dankserver services by published LAN
port (192.168.0.235:PORT).

- dankclaw/Caddyfile: merged vhost list (was: two Caddyfiles, one per
  host). gitea's single vhost moves in from the retired gitea-caddy
  quadlet (option B). Upstreams fixed to live reality: gitea :3000,
  search :8081 (gluetun), chat :3005 (open-webui quadlet), media
  services via 192.168.0.235. Retired: llms (open-webui on dankserver
  is removed; chat is its successor), the dankserver homepage (302 to
  the shared one).
- dankserver/Caddyfile, dankclaw/gitea/Caddyfile: deleted (git rm).
- dnsmasq addn-hosts: every name -> 192.168.0.143 (the one Caddy
  host); no .235 records, no llms.
- compose.yaml: removed the openwebui/postgres/gitea services (they
  are quadlets now); caddy service mounts the rendered Caddyfile from
  /home/dank/Containers/caddy, healthcheck probes the bare-IP
  catch-all (302).
- deploy.sh: per-target logic (dankserver = pull-only, health via the
  shared Caddy at 192.168.0.143); renders CADDY_BASICAUTH_HASH from
  the host .env into the mounted Caddyfile.
- .env.example: CADDY_BASICAUTH_HASH on dankclaw; Porkbun block
  dropped from dankserver (its Caddy is gone); SEARXNG_BASE_URL to
  the flat name; stale OPENAI_* block dropped (quadlet is
  self-contained).
- tests: caddyfile.bats now asserts the single-Caddyfile invariant;
  dnsmasq.bats cross-checks vhosts -> .143 and the retired names.
2026-08-21 21:44:18 -07:00
Frank 1dee2b737e dnsmasq: serve config from /home/dank/Containers/dnsmasq (rootless-correct)
/opt is root-owned; the established data-location pattern (see
docs/Architecture/Storage Policy.md, open-webui quadlet) is
/home/dank/Containers/<service>.
2026-08-21 16:41:49 -07:00
Frank 55d062ea7b DNS names: flat franksdank.cloud scheme + dnsmasq LAN DNS on dankclaw
Phase 1 of the local-DNS-name work. Users type a name, not a port.

- dnsmasq quadlet on dankclaw (rootless, slirp4netns, port 53 published):
  serves the flat name scheme from addn-hosts, upstream 1.1.1.1/1.0.0.1.
  Image built from the in-repo Containerfile (localhost/dnsmasq:latest).
- Caddyfiles (both hosts): all *.dankclaw.local / *.dankserver.local vhosts
  renamed to flat <service>.franksdank.cloud. Explicit http:// site
  addresses for now; TLS (wildcard LE via DNS-01/Porkbun) lands in phase 2.
- Drift reconciliation (live /etc/caddy/Caddyfile on dankserver differs
  from the repo): added the live-only llms.franksdank.cloud vhost
  (OpenWebUI on :3000). Dropped the legacy search vhost from dankserver
  (the name belongs to dankclaw's gluetun-routed SearXNG; the legacy
  instance stays reachable at 192.168.0.235:8081).
- Homepage service links updated to the new names.
- New test seams (TDD): tests/dnsmasq.bats (quadlet boot-persistence,
  port 53, conf/record validity, Caddyfile<->addn-hosts cross-check on
  the right host IPs); caddyfile.bats naming-convention tests updated.

LAN clients get this DNS from the SURFboard G36 (DNS Relay -> statically
configured -> 192.168.0.143); the router change is a pending user step.
2026-08-21 16:31:42 -07:00
Frank 833f2365f1 env: add Porkbun API token placeholders (DNS-01 cert issuance)
Real tokens live only in the host .env files (git-ignored):
- /home/dank/serverAppConfigs/dankclaw/.env
- /home/dank/serverAppConfigs/dankserver/.env

Token is scoped to the franksdank.cloud zone (DNS read/write).
2026-08-21 16:00:16 -07:00
Frank e8fd009419 docs: fact-check sweep, Jellyfin, *arr stack, and ROM scraping guides
Fact-check results:
- RomM 5.2.0: MariaDB+Redis now required (was SQLite), auth var renamed, 11 metadata providers
- Copyparty v1.20.19: 3 CVEs fixed, WOPI integration, docker editions documented
- Calibre-Web 0.6.27-ls397: version pinning, kepubify auto-config, CVE fixes
- Calibre 9.9.0: content server security fix noted

New documentation:
- docs/Dankserver/Jellyfin.md — full media server guide (compose, Caddy, GPU, metadata, clients)
- docs/Dankserver/ARR-Stack.md — automated download stack overview
- docs/Dankserver/RomM-Scraping.md — metadata providers, naming, cover art, emulator integration
- docs/Dankserver/sonarr.md, radarr.md, lidarr.md, readarr.md, prowlarr.md, qbittorrent.md
- docs/Dankserver/mariadb.md, redis.md — supporting services for RomM

Infrastructure updates:
- dankserver/compose.yaml: added mariadb, redis, jellyfin, *arr services
- dankserver/Caddyfile: added hostnames for all new services
- dankserver/.env.example: added all new environment variables
- docs/index.md: linked all new documentation pages

Tested: 52/52 tests pass.
2026-08-20 23:40:07 -07:00
Frank e3eb5334cb feat: initialize serverAppConfigs with podman stacks for Dankserver and Dankclaw
- Dankserver stack: RomM, Copyparty, Calibre-Web, node-exporter
- Dankclaw stack: Gluetun VPN (SearXNG tunnel), SearXNG+Valkey, ComfyUI
  (CPU backend), OpenWebUI, Hermes, Gitea+Postgres (migrated), Caddy
  reverse proxy, Grafana+Prometheus+Loki+Alloy observability
- Caddyfiles with .local hostname convention and basicauth homepage
- bind-mount storage policy, no named volumes
- deploy.sh mirroring vllmScripts git workflow (push -> pull -> restart -> health)
- docs/ Obsidian vault: per-service docs from frank-default-small research
  subagents (promtail EOL -> Alloy; gitea SECRET_KEY preservation; ComfyUI --cpu)
- bats test suite at 6 seams — 22 tests green
2026-08-13 20:19:41 -07:00