stacks/task-board/compose.yaml + .env.example describe the runtime —
image tag is task-board:local (built on the host), 7878 on host maps
to 7878 in container, SQLite lives at /opt/docker/conf/task-board/data/
(bind mount, uid 1000 friendly), homepage auto-card labels under
Toolchain group, on traefik-net like the rest of the fleet.
playbooks/deploy-task-board.yaml is the first real elway playbook —
exercises everything we built tier-1 + tier-2 idempotency for:
- `creates:` on mkdir + first-time clone + compose dir + data dir
- `when:` to chown /opt/docker/build only if it came up root-owned
- `when:` to seed .env only if one doesn't already exist (never
clobbers user edits on rerun)
- `changed_when:` on the `git reset --hard` step so repeat runs
against the same ref report `ok` instead of `changed`
- `changed_when: "false"` on every verify step (they attest, not
change)
- `upload:` with mode for compose.yaml + .env
Post-up the playbook polls /api/health for 30s before handing off to
the verify phase, so verification doesn't race the healthcheck's
start_period. Verify covers: /api/health 200, /api/tasks shape, /mcp
reachable, container on traefik-net.
Prereqs documented in the playbook header: Docker + compose plugin,
traefik-net network, git SSH access to gitea from the target host.
The original smoke-test curl used voice="default" which doesn't
exist — the neosun wrapper ships zero preset voices, and the
built-in SFT speakers (中文男/女 etc.) are not surfaced. Calling
/v1/audio/speech with any unregistered voice returns a 400 whose
JSON body curl happily writes into the .wav (124-byte phantom).
Replaced the smoke test with the full clone → synthesize flow and
added a gotchas section covering:
- No default voice; /v1/voices/create is mandatory
- Reference audio ≤30s (frontend asserts; longer clips 500 at
synthesis time, not at upload)
- Providing an explicit transcript beats the auto-ASR fallback
- voice_id is the handle, not name
- Both cosyvoice-v3 and cosyvoice-v2 ship in the image
Also documented streaming: available via /api/tts with stream=true
(~150ms TTFB), NOT on /v1/audio/speech. Clarified field-name
differences between the OpenAI-compat and native endpoints in a
table. No WebSocket / SSE in this wrapper despite upstream support.
The Shadowfita FastAPI wrapper hit two unfixed upstream bugs on the
first real /transcribe call — chunker return-shape mismatch (open
issue #16) and a `torchaudio.tensor` that doesn't exist (open #10).
Rather than babysit someone else's half-tested code, switched to
sherpa-onnx with the prebuilt int8 Parakeet-TDT tarball from k2-fsa,
and wrote our own ~60-line FastAPI wrapper.
Moving parts now owned in-tree:
Dockerfile CUDA 12.8 + cuDNN 9 runtime base, installs
sherpa-onnx==1.12.39+cuda12.cudnn9 + fastapi +
soundfile + libasound2 (sherpa-onnx links to ALSA
at load time even when we never touch a mic).
app.py OfflineRecognizer.from_transducer() once at startup;
/transcribe and /v1/audio/transcriptions both accept
multipart uploads and return {"text": ...}.
entrypoint.sh Idempotent model download to /models on first run
(~400 MB int8 tarball), then exec uvicorn.
Smoke test: 0.wav (bundled in the tarball, The House of the Seven
Gables excerpt) transcribes cleanly in ~1.2s on GPU.
PARAKEET_MODEL_URL in .env lets you swap to the v3 (25-language)
tarball without touching any other files. Wipe *.onnx + tokens.txt
from the models dir and the entrypoint re-downloads.
Two new speech stacks on irv-ml1, both on the /worktank/<stack>/
pattern, no tnet (irv-ml1 is local-endpoints-only for now).
parakeet — ASR via Shadowfita/parakeet-tdt-0.6b-v2-fastapi:
- docker buildx git context pinned to SHA 31c5652; no source
vendored. Rebuild on SHA bump.
- GPU-capable FastAPI + Silero VAD + WS streaming.
- API: POST /transcribe, WS /ws/transcribe, GET /healthz. Not the
literal OpenAI `/v1/audio/transcriptions` path — note in README.
- HF cache at /worktank/parakeet/models/ (excluded from restic).
- Build ~158s first time; steady-state start ~40s.
cosyvoice — TTS via neosun/cosyvoice:v1.3.2 shipping
Fun-CosyVoice3-0.5B-2512 (CosyVoice 3, chosen over v2 for the
expanded 5,000-hour instruction-following data covering emotions,
speed, tones, dialects, accents, role-playing; ~150ms streaming
TTFB matches v2). API: /v1/audio/speech (OpenAI drop-in),
/v1/voices/create (cloning), /health.
- Host port 8190 (container 8188; host 8188 already taken by comfyui).
- /worktank/cosyvoice/{voices,input,output}/; voices include in
restic (precious — reproducing a clone needs the original ref
audio), input+output excluded (scratch).
- Model weights (~2-3 GB) live inside image layer; re-download on
tag bump, persist across `compose up -d`.
Both healthy on first deploy.
New stack mirroring PFI convention (stacks/comfyui/) using
mmartial/comfyui-nvidia-docker:ubuntu24_cuda12.8-20260312. Both GPUs
exposed, pinned to CUDA 12.8 to match the host's 570.x driver and the
native cuda-toolkit already in place.
Layout — single tree under /worktank/comfyui/ (462G dedicated, 1%
used pre-deploy):
- basedir/ → /basedir user state (models, workflows, custom_nodes,
input, output); owned 1000:1000 so external
tools can edit workflow JSON directly.
- run/ → /comfy/mnt ComfyUI source + venv + pip cache (~7.8G
after bootstrap). Bind mount instead of
named volume — the image refuses to chown
mounted paths at startup, so keeping this
lkraven-owned avoids the sudo dance.
servers/irv-ml1/README.md refreshed: Docker upgraded to 29.4.1 with
traefik-net in place; dockge + beszel-agent + dozzle-agent already
present; /storetank dropped 92% → 64%; restic coverage to
rest-server-nh3 is operational (not "currently none" as prior text).
Adds a new `pinned` group with swap: false (models coexist in VRAM),
exclusive: false (group shares with other groups), persistent: true
(never unload). Each member also gets ttl: 0 so the per-model
idle-timeout can't drop them either — belt + suspenders.
Pair is currently qwen3.5-9b (~6 GB Q4) + qwen3.6-35-a3b (~29 GB Q6).
Plus the 128K KV caches, roughly 50-60 GB VRAM resident. Appropriate
for an A6000/H100-class card; verify fit after deploy.
Committed as a canonical change; push + restart still needed on
ana-ml2.
rest-server-ana README now describes the /mnt/backup/restic/repo/
top-level NFS mount and its three per-site subdirs:
ana/ — live data served by this rest-server (per-host repos +
.htpasswd) — what DATA_DIR points at
esh/ — mirror destination for ESH-site backups (pending)
nh3/ — mirror destination for NH3 Synology's tree (pending)
ana-ml2 README gains a proper "Recreating the repo" section with the
correct /mnt/backup/restic/repo/ana/ana-ml2/ path for wiping the old
repo after a lost passphrase, and two paths for regenerating keys:
- interactive: type a user-generated passphrase at restic's init
prompt, then install it into /etc/restic/password via `cat > file`
+ Ctrl-D (no shell history or transcript exposure)
- scripted: openssl rand -base64 48, passphrase prints once and must
be captured into the password manager immediately
Cross-site replication snippet in rest-server-ana README updated to
use the unified /mnt/backup/restic/repo/{esh,nh3}/ destinations
instead of the earlier restic-mirror-*/ staging paths.
services.yaml:
- ANA-Firewall: href + siteMonitor both point at the IP now (was href
to FQDN but siteMonitor to IP — inconsistent)
- PFI-VM-Docker, NH3-SW1, NH3-VM-Docker, ESH-VM-Docker: upgrade from
`ping:` to `siteMonitor:` against the href URL so the up/down dot
reflects whether the web UI actually responds, not just ICMP
dockge canonical:
- icon sh-dockge.png (was si-portainer — wrong project). Applies to all
five fleet Dockge instances once their compose files are redeployed.
Per-group colors investigated but not supported by homepage (only site-
wide `color:` exists); skipped.
CWA image ships curl and busybox nc but no wget, so the initial
healthcheck wedged the container in unhealthy state. Swap to curl -fsS
hitting the login page; compose-time substitution of CWA_PORT keeps
custom port overrides working.
Retire the linuxserver.io calibre + calibre-web pair. CWA
(crocodilestick/calibre-web-automated) is an all-in-one: combined web UI
with Calibre backend, auto-ingest via drop folder, scheduled metadata
maintenance — single container on :8083.
- Reuses the existing library at /mnt/books/calibre/calibre_library
(NFS) and the existing calibre-web config at /opt/docker/conf/calibre-web,
so user accounts and the book catalogue carry over.
- Requires NETWORK_SHARE_MODE=true (library is on NFS; SQLite WAL would
corrupt otherwise).
- New drop-zone at /mnt/books/calibre/ingest; files placed there are
ingested into the library and then removed.
- Loses the LSIO desktop Calibre GUI (ports 8082/8181/8281) — run
Calibre locally if you need the desktop UI.
Old compose dirs (/opt/docker/compose/{calibre,calibre-web}) removed on
esh-docker-vm; old images can be pruned at leisure.
vm-esh-nas (10.0.50.154) is a NAS-adjacent Docker VM on the esh-pve-nas
hypervisor. Runs filezilla (port 5800), dockge, beszel-agent, dozzle-agent
with /mnt/{share,music,books,media} NFS-mounted from 10.0.50.50.
Use this host when a stack needs direct NFS mounts to the ESH NAS shares.
Canonicalize dockge as stacks/dockge/ — single compose used on all five
Docker hosts with per-host DOCKGE_HOST_LABEL/DOCKGE_HOST_IP in .env so
each card on the homepage points at its own instance. Labeled
homepage.group=Service Networking.
Beszel + Dozzle agent dirs also renamed to beszel-agent-<site> /
dozzle-agent-<site> pattern across the fleet for consistency.
Reorganize the gethomepage dashboard from site-based (PFI-ANA, ESH, NH3)
to function-first grouping (Monitoring, AI Systems, Apps, Media, Games,
Infra-<site>, Service Networking). Canonical config now tracked in
configs/homepage/ with Plex/Jellyfin widget keys moved to env
substitution.
Label sweep across fleet compose files:
- beszel, dozzle, backrest -> Monitoring
- rest-server-ana -> Service Networking
Healthcheck fixes (previous wget/curl paths broke on distroless +
--private-repos 401):
- beszel hub: /beszel health --url ...
- beszel agent: /agent health (newly added)
- rest-server: nc -z localhost 8000 (TCP probe)
Group name originally "Wiring / Plumbing" collapsed to single-word group
on homepage's parser; renamed to "Service Networking" everywhere.
Network probes (no web admin, Debian SSH banner, only file-server ports
open) show 10.250.50.50 is vanilla Debian 12 with hand-configured NFS/SMB,
reprovisioned from the original TrueNAS SCALE install. Update stack
comments, README storage notes, Backrest description, proxmox-vms.md
entry for VM 100, and the restic configs intro to match.
Cross-site sync plan simplifies to plain rsync over SSH on both sides —
no appliance-specific tooling needed.
Captures the full workspace state built up to this point:
- CLAUDE.md + README.md describing conventions and the four-host fleet
(ana-ml2, ana-docker, nh3-docker, esh-docker-vm).
- Per-host notes under servers/<host>/ with ssh-target fallback files
and latest system-details snapshots (two in-compose credential leaks
scrubbed; the upstream compose files still need to move those to .env).
- scripts/: server_inspect.sh (read-only remote diagnostic),
refresh-server-info.sh (dir-driven discovery + snapshot capture with
validation warnings), add-host.sh, sync-stacks.sh (pull
compose/conf trees), deploy-stack.sh (push with per-file diff + prompt).
- stacks/: canonical compose for backrest, beszel, dozzle, llama-swap,
rest-server-ana, rest-server-nh3, vllm-qwen3, plus the retired
infinity reference. All use the .env-driven + traefik-net + homepage
label pattern.
- configs/restic/ana-docker/: first resticprofile config + pre-backup
hook (Synapse pg_dump, Seafile mysqldump, Vaultwarden SQLite); templates
for the other three hosts to come.
- docs/pfi/: general infrastructure reference carried over.
- .gitignore excludes .env, stacks-mirror/, and assorted secret/state
filenames to prevent re-leaks on later commits.