feat(deploy): stand up correspondence-nuc as a second instance
Some checks are pending
Test and SonarQube / quality (push) Waiting to run

The NFS blocker from TODO.md's deployment entry is resolved (export
set up by the user on the DGX); picks up from there and completes the
deployment end to end.

- Mounted the DGX's TTS_Voices export read-write on the NUC at
  /mnt/dgx-tts-voices, persisted in /etc/fstab -- confirmed writable
  and confirmed the NUC instance sees the DGX's real voice library
  (344 voices), not a separate copy.
- Deployed by rsyncing the working tree from the DGX (excluding .git,
  config/, logs/ and other local-only paths) and building the image
  directly on the NUC, rather than pulling ghcr.io/martin-b78/... --
  that registry copy turned out to be both several versions stale and
  private with no credentials configured on the NUC, and the source
  was already right there to build from.
- Found and fixed a real gap while wiring it up: vision_ocr_url and
  vision_ocr_fallback_url default to host.docker.internal, which
  resolves to whichever host the container is running on -- correct
  on the DGX itself, silently wrong on the NUC (would have pointed at
  the NUC's own nonexistent OCR engine). Pinned both explicitly to the
  DGX's LAN IP via environment overrides.
- The container crash-looped on first start (sqlite3.OperationalError:
  unable to open database file) because docker compose had created
  config/ and logs/ root-owned under this session's root SSH access to
  the NUC, while the image's app user is uid 1000 -- chown -R 1000:1000
  fixed it.
- New docker-compose.nuc.yml, tracked here as the reviewable reference
  for that host (docker-compose.yml stays the DGX's own -- different
  build/network/socket-proxy shape). Deployed to the NUC as its own
  docker-compose.yml, since that host has no .git checkout to diff
  against, only the rsynced tree.

Verified: container stable (no restart loop), HTTP 200, settings
confirm all four engine URLs correctly resolve to the DGX, voice count
matches the DGX exactly, restart policy is unless-stopped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-14 10:06:55 +02:00
parent c91aaabe3a
commit 4249382ac6
5 changed files with 118 additions and 22 deletions

View File

@ -5,6 +5,16 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.28.0] — 2026-09-14
### Added
- **Second deployment: `correspondence-nuc` (192.168.178.19), live alongside the DGX Spark instance.** No TTS/OCR engines of its own — every generation and OCR call crosses the LAN to the DGX, which required pinning `vision_ocr_url`/`vision_ocr_fallback_url` explicitly (their default, `host.docker.internal`, resolves to whichever host the container happens to run on, which is silently wrong off the DGX). Voice library is NFS-mounted read-write from the DGX's own directory (`/etc/exports` on the DGX, persisted in `/etc/fstab` on the NUC) — confirmed the NUC instance sees the exact same 344 voices, never a separate copy that could drift.
- Deployed by building directly on the NUC from an rsynced copy of the working tree, rather than pulling the published `ghcr.io` image — that registry copy turned out to be both several versions stale and private with no credentials configured on the NUC.
- New `docker-compose.nuc.yml`, tracked in this repo as the reviewable reference for that host (the existing `docker-compose.yml` stays the DGX's own — different build/network/socket-proxy shape).
- Accepted limitation carried over from planning: a voice change still needs a manual restart of the DGX's TTS containers — no `docker-socket-proxy` on the NUC instance, since cross-host container control was never going to work through it anyway.
---
## [1.27.5] — 2026-09-13 ## [1.27.5] — 2026-09-13
### Fixed ### Fixed

View File

@ -10,10 +10,22 @@ features are mature and in daily use.
## Deployment ## Deployment
Self-hosted Docker on the DGX Spark, port 7890, image Two self-hosted Docker instances, both port 7890:
`ghcr.io/martin-b78/tts-voice-creator-clone-and-design-2`.
Python and static files are bind-mounted, so code changes need only a container - **DGX Spark** (`192.168.178.8`) — primary. `docker-compose.yml` builds locally and tags the result
recreate; rebuild and push only when `Dockerfile` or `requirements.txt` change. `ghcr.io/martin-b78/tts-voice-creator-clone-and-design-2`, but that registry copy is not kept current on every
deploy (several versions behind as of 2026-09-14) — treat it as a snapshot, not a source of truth. Python and
static files are bind-mounted, so a code change needs only a container recreate; rebuild only when `Dockerfile`
or `requirements.txt` change.
- **`correspondence-nuc`** (`192.168.178.19`) — secondary, live since 2026-09-14. No TTS/OCR engines of its own;
every generation call crosses the LAN to the DGX (`docker-compose.nuc.yml`, tracked here, pins
`vision_ocr_url`/`vision_ocr_fallback_url` explicitly to the DGX's IP — their default resolves relative to
whichever host the container runs on, silently wrong here otherwise). Voice library is the DGX's own directory,
NFS-mounted read-write (`/etc/exports` on the DGX, `/etc/fstab` on the NUC) — never a separate copy that could
drift. Deployed by rsyncing the working tree (excluding `.git`, `config/`, `logs/`) and building on the NUC
itself, since the `ghcr.io` image is both stale and private with no registry login there. See TODO.md's
"P2 — deployment" entry for the full setup and the one accepted limitation (voice-change auto-restart of the
DGX's TTS containers doesn't reach across hosts — manual restart on the DGX, same as before this existed).
## Layout ## Layout

40
TODO.md
View File

@ -97,23 +97,29 @@ architecture change: the larger Audiobook/Rehearser/style split.
### P2 — deployment ### P2 — deployment
- [ ] **S — Deploy the app to `correspondence-nuc` (192.168.178.19), pointed at the DGX Spark's TTS engine over the - [x] **S — Deploy the app to `correspondence-nuc` (192.168.178.19), pointed at the DGX Spark's TTS engine over the
network.** Blocked on sudo access on the DGX (no passwordless sudo in the Claude session): needs network.** Live since 2026-09-14, `v1.27.5`. NFS export set up on the DGX (`/home/sparky/Media/_Sounds/TTS_Voices`
`nfs-kernel-server` installed and `/home/sparky/Media/_Sounds/TTS_Voices` exported read-write to → `192.168.178.19` only, `rw,no_root_squash`), mounted read-write on the NUC at `/mnt/dgx-tts-voices` and
`192.168.178.19` only, so the NUC's voice library never drifts from what the DGX's TTS engine actually has on persisted in `/etc/fstab` there — confirmed the NUC instance sees the same 344 voices the DGX does, not a
disk (the NUC only NFS-mounts Synology shares today, nothing from the DGX). `tts_url`/`vision_ocr_url` settings separate copy. Deployed by rsyncing the working tree from the DGX (excluding `.git`, `config/`, `logs/` and other
already default to the DGX's LAN IP (`192.168.178.8`) and OpenAI-compatible network calls, so once the mount local-only paths) to `/home/sparky/Docker/tts-voice-creator-clone-and-design-2` on the NUC and building the image
exists this is mostly a new compose file + `docker compose up -d` on the NUC. Accepted limitation: voice-change there directly — the published `ghcr.io` image turned out to be both several versions stale and private with no
auto-restart of the DGX's TTS containers (`TTS_CONTAINER_NAME(S)`) won't work cross-host from a NUC-hosted app registry credentials on the NUC, not worth setting up for an image that would need re-pushing on every deploy
instance, since `docker-socket-proxy` there only controls the NUC's own Docker socket — that's a manual restart anyway when the source is right there to build from.
on the DGX after a voice change, same as today. **One real gap found along the way:** `vision_ocr_url`/`vision_ocr_fallback_url` default to
Commands needed on the DGX (run manually, or grant this session sudo to run them): `host.docker.internal`, which resolves to *whichever host the container is running on* — correct on the DGX
``` itself, silently wrong on the NUC (would have pointed at the NUC's own, nonexistent OCR engine). Pinned both to
sudo apt-get install -y nfs-kernel-server the DGX's LAN IP explicitly via environment overrides in the NUC's compose file.
echo "/home/sparky/Media/_Sounds/TTS_Voices 192.168.178.19(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports **Also needed:** the NUC's `config`/`logs` bind-mount directories were created root-owned by `docker compose`
sudo exportfs -ra (this session's NUC access is a root SSH key) while the container's own `app` user is uid 1000 — `chown -R
sudo systemctl enable --now nfs-kernel-server 1000:1000` on both before the app could open its SQLite state DB. Confirmed the accepted limitation from before
``` still holds as expected: no `docker-socket-proxy` on the NUC instance (nothing local for it to control, and
cross-host container restart never worked anyway), so a voice change still needs a manual restart on the DGX.
NUC compose file tracked here as `docker-compose.nuc.yml` (the plain `docker-compose.yml` in this repo stays the
DGX's own, per its `build:`/`dgx_net`/`docker-socket-proxy` shape) — deployed to the NUC as its
`docker-compose.yml`, since that host has no `.git` checkout of its own to diff against, only the rsynced tree.
A code change means: re-rsync the tree (excluding `.git`, `config/`, `logs/`), `docker compose build`, `docker
compose up -d` — no separate push/pull step, same as the DGX's own bind-mount-and-recreate pattern.
## Deferred — public multi-user readiness (cross-app initiative) ## Deferred — public multi-user readiness (cross-app initiative)

View File

@ -1 +1 @@
1.27.5 1.28.0

68
docker-compose.nuc.yml Normal file
View File

@ -0,0 +1,68 @@
services:
tts-voice-creator-clone-and-design-2:
# Built locally rather than pulled: the ghcr.io image is private (this
# host has no registry credentials) and, checked on the DGX, several
# versions stale besides. The full source is already deployed here
# (see the deploy step that rsyncs it from the DGX) -- build directly
# from it instead of standing up registry auth for a copy that would
# need pushing to stay current anyway.
build:
context: .
dockerfile: Dockerfile
container_name: tts-voice-creator-clone-and-design-2
restart: unless-stopped
# Same hardening as the DGX deployment: image filesystem stays
# immutable, the app writes only to the explicit mounts below.
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:size=1g,mode=1777
mem_limit: "${APP_MEM_LIMIT:-4g}"
mem_reservation: "${APP_MEM_RESERVATION:-1g}"
ports:
- "7890:7890"
volumes:
# NFS-exported from the DGX (see /etc/exports there) so this instance
# shares exactly the voice library the DGX's TTS engine has on disk --
# never a separate copy that could drift.
- ${VOICE_HOST_DIR:-/mnt/dgx-tts-voices}:/voices:rw
- ./config:/home/app/.config/tts-voice-creator:rw # settings, presets, routes
- ./logs:/logs:rw # application logs
# Live-editable without a rebuild -- same pattern as the DGX compose.
- ./static:/app/static:ro
- ./server.py:/app/server.py:ro
- ./core:/app/core:ro
- ./routes:/app/routes:ro
- ./workflows:/app/workflows:ro
- ./VERSION:/app/VERSION:ro
- ./CHANGELOG.md:/app/CHANGELOG.md:ro
environment:
- PYTHONUNBUFFERED=1
- VOICES_DIR=/voices
- OUTPUT_DIR=/voices/active_voices
# Most engine defaults already point at the DGX's real LAN IP and work
# unmodified from any host. Vision OCR is the one exception: its
# default is host.docker.internal, which resolves to whichever HOST
# the container happens to run on -- correct from the DGX itself, but
# on the NUC that would mean the NUC's own (nonexistent) OCR engine
# rather than the DGX's. Pin both explicitly to the DGX.
- VISION_OCR_URL=http://192.168.178.8:8098/v1
- VISION_OCR_FALLBACK_URL=http://192.168.178.8:8099/v1
#
# No docker-socket-proxy / DOCKER_HOST here on purpose: this instance
# has no local Docker socket to control, and the DGX's TTS containers
# can't be restarted cross-host through it either way. Voice-change
# auto-restart is an accepted manual step on the DGX after a change --
# same as documented for the DGX deployment itself. Docker-dependent
# diagnostics on this instance report unavailable rather than failing.
extra_hosts:
- "host.docker.internal:host-gateway"