feat(deploy): stand up correspondence-nuc as a second instance
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
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:
parent
c91aaabe3a
commit
4249382ac6
10
CHANGELOG.md
10
CHANGELOG.md
@ -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
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -10,10 +10,22 @@ features are mature and in daily use.
|
||||
|
||||
## Deployment
|
||||
|
||||
Self-hosted Docker on the DGX Spark, port 7890, image
|
||||
`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
|
||||
recreate; rebuild and push only when `Dockerfile` or `requirements.txt` change.
|
||||
Two self-hosted Docker instances, both port 7890:
|
||||
|
||||
- **DGX Spark** (`192.168.178.8`) — primary. `docker-compose.yml` builds locally and tags the result
|
||||
`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
|
||||
|
||||
|
||||
40
TODO.md
40
TODO.md
@ -97,23 +97,29 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
||||
|
||||
### P2 — deployment
|
||||
|
||||
- [ ] **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
|
||||
`nfs-kernel-server` installed and `/home/sparky/Media/_Sounds/TTS_Voices` exported read-write to
|
||||
`192.168.178.19` only, so the NUC's voice library never drifts from what the DGX's TTS engine actually has on
|
||||
disk (the NUC only NFS-mounts Synology shares today, nothing from the DGX). `tts_url`/`vision_ocr_url` settings
|
||||
already default to the DGX's LAN IP (`192.168.178.8`) and OpenAI-compatible network calls, so once the mount
|
||||
exists this is mostly a new compose file + `docker compose up -d` on the NUC. Accepted limitation: voice-change
|
||||
auto-restart of the DGX's TTS containers (`TTS_CONTAINER_NAME(S)`) won't work cross-host from a NUC-hosted app
|
||||
instance, since `docker-socket-proxy` there only controls the NUC's own Docker socket — that's a manual restart
|
||||
on the DGX after a voice change, same as today.
|
||||
Commands needed on the DGX (run manually, or grant this session sudo to run them):
|
||||
```
|
||||
sudo apt-get install -y nfs-kernel-server
|
||||
echo "/home/sparky/Media/_Sounds/TTS_Voices 192.168.178.19(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
|
||||
sudo exportfs -ra
|
||||
sudo systemctl enable --now nfs-kernel-server
|
||||
```
|
||||
- [x] **S — Deploy the app to `correspondence-nuc` (192.168.178.19), pointed at the DGX Spark's TTS engine over the
|
||||
network.** Live since 2026-09-14, `v1.27.5`. NFS export set up on the DGX (`/home/sparky/Media/_Sounds/TTS_Voices`
|
||||
→ `192.168.178.19` only, `rw,no_root_squash`), mounted read-write on the NUC at `/mnt/dgx-tts-voices` and
|
||||
persisted in `/etc/fstab` there — confirmed the NUC instance sees the same 344 voices the DGX does, not a
|
||||
separate copy. Deployed by rsyncing the working tree from the DGX (excluding `.git`, `config/`, `logs/` and other
|
||||
local-only paths) to `/home/sparky/Docker/tts-voice-creator-clone-and-design-2` on the NUC and building the image
|
||||
there directly — the published `ghcr.io` image turned out to be both several versions stale and private with no
|
||||
registry credentials on the NUC, not worth setting up for an image that would need re-pushing on every deploy
|
||||
anyway when the source is right there to build from.
|
||||
**One real gap found along the way:** `vision_ocr_url`/`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 to
|
||||
the DGX's LAN IP explicitly via environment overrides in the NUC's compose file.
|
||||
**Also needed:** the NUC's `config`/`logs` bind-mount directories were created root-owned by `docker compose`
|
||||
(this session's NUC access is a root SSH key) while the container's own `app` user is uid 1000 — `chown -R
|
||||
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)
|
||||
|
||||
|
||||
68
docker-compose.nuc.yml
Normal file
68
docker-compose.nuc.yml
Normal 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"
|
||||
Loading…
Reference in New Issue
Block a user