Files
esh-pfi-infrastructure/stacks/waterland-studio/compose.yaml
T
vh e6907819b0 feat(waterland-studio): deploy b72425b — all three upstream findings fixed
One update.sh run on irv-ml1 carried both open upstream PRs, per the
operator's green-light on the job-store fix:

- #5 (464dfc2) declares cupy-cuda12x[ctk] on the gpu extra and takes uv
  out of the render path (sys.executable -m waterland.cli), retiring the
  runtime prune trap at the source.
- #6 (b72425b) rehydrates the job index from the data volume at startup,
  fixing the unbounded store growth reported from this side.

Verified after the update rather than assumed: healthy on backend cupy;
/api/jobs went 1 -> 16 against 16 directories on disk, so API and volume
agree for the first time; nothing wrongly reclaimed, correct since 16 is
under RETAIN=40 and adoption only makes them visible; a real 256^2 plate
render completes warm, so the kernel-cache volume survived the image swap.
A subsequent render took both counts to 17.

The image keeps its explicit [ctk] install and UV_NO_SYNC/UV_OFFLINE pins
even though both are now redundant. The header requirement is a property
of this slim base, not of the upstream extra, and the cost is measured
rather than assumed: uv sync satisfies it first, so the line reports
"Audited 1 package" and adds 0.3s to the build. The env pins are now
cheap defence-in-depth against any future path that re-enters uv.

Docs corrected in place: the README's upstream-finding section is now a
resolved-finding record, and the two "bounded ~500 MB" claims say which
commit made that bound hold across restarts rather than only within a
process. Comment-side changes pushed to the live compose dir; no restart
was needed for them.
2026-08-19 01:39:13 -07:00

100 lines
4.5 KiB
YAML

# waterland-studio — watercolour render service on irv-ml1, port 8410.
#
# FastAPI + vanilla-JS SPA fronting the waterland CLI: upload an image, get a
# watercolour plate and a painted-in reveal animation. Every job shells out to
# the CLI, which runs a fluid simulation on the A6000.
#
# Handed over by waterland-dev 2026-08-19, replacing a bare `nohup` that would
# not have survived a reboot.
#
# ⚠️ THE BUILD CONTEXT LIVES OUTSIDE THIS DIRECTORY, DELIBERATELY.
# /opt/waterland-studio/src is a checkout of vh/waterland @ main.
# `deploy-stack.sh` rsyncs this stack dir with --delete, so a checkout kept
# in here would be destroyed on the next deploy. Refresh it with ./update.sh.
#
# ⚠️ SERIAL BY DESIGN — ONE REPLICA, ONE CARD. A render is 20-45s of near-full
# GPU and the app runs a single worker thread. Two of these on the same A6000
# would OOM or thrash. Throughput is a conversation about hardware, not about
# replica count (waterland-dev, explicitly).
#
# ⚠️ NO AUTHENTICATION, AND IT ACCEPTS ARBITRARY FILE UPLOADS. It must stay
# inside the LAN / WireGuard boundary. Do NOT paper over this by putting it
# behind a proxy with a password — waterland-dev has offered to add a real
# auth layer if it ever needs wider reach. Ask, don't improvise.
name: waterland-studio
services:
waterland-studio:
build:
# Absolute paths: the context is the source checkout, the Dockerfile is
# this version-controlled one, and the two live in different trees.
context: /opt/waterland-studio/src
dockerfile: /opt/docker/compose/waterland-studio/Dockerfile
image: waterland-studio:local
container_name: ${WLS_CONTAINER:-waterland-studio}
restart: unless-stopped
ports:
- "${WLS_PORT:-8410}:8410"
volumes:
# Job store: uploaded sources plus rendered plates and animations.
# ~12 MB per job with an animation; the app self-evicts at 40 retained
# jobs (RETAIN in studio/jobs.py), so steady state is bounded ~500 MB.
# That bound holds ACROSS restarts only as of b72425b, which rehydrates
# the job index from this volume at startup; before that, eviction was
# blind to anything a restart had orphaned. See the README.
# Scratch output, not source-of-truth — losing it costs a re-render.
- waterland_studio_data:/data
# cupy JIT kernel cache. Not optional for good behaviour: cupy compiles
# its kernels through NVRTC on first use, and measured on this host that
# cold compile costs ~17s — the first 256^2 render after a fresh
# container took 23.3s against 6.1s warm. Without this volume every
# restart makes the next user wait 4x, and it looks like the service is
# slow rather than warming up.
- waterland_studio_kernels:/root/.cupy
environment:
- WATERLAND_STUDIO_DATA=/data
- WATERLAND_STUDIO_REPO=/app
- WATERLAND_STUDIO_BACKEND=${WLS_BACKEND:-cupy}
# 0, not 1 — see the Dockerfile. Exactly one GPU is exposed below, so
# inside this container the A6000 is index 0 under PCI_BUS_ID ordering.
- CUDA_VISIBLE_DEVICES_TARGET=${WLS_CUDA_TARGET:-0}
deploy:
resources:
reservations:
devices:
- driver: nvidia
# "1" is the A6000 in DOCKER's device ordering, matching the
# comfyui stack on this host. Device 0 is the 3090, which hosts
# the TTS zoo and must not be touched.
device_ids: ["${WLS_GPU_ID:-1}"]
capabilities: [gpu]
healthcheck:
# /api/health touches no GPU and is safe to poll. The interval is
# deliberately loose: a render holds the GPU for 20-45s and the app's own
# job timeout is 480s, so an aggressive probe would be measuring queue
# depth rather than liveness.
test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8410/api/health', timeout=5).status==200 else 1)\""]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
networks:
- tnet
labels:
- homepage.group=AI - Image & Media
- homepage.name=Waterland Studio
- homepage.icon=mdi-watercolor
- homepage.description=Watercolour plate + reveal animation renderer (irv-ml1, A6000)
- homepage.href=http://10.100.79.3:${WLS_PORT:-8410}/
- homepage.siteMonitor=http://10.100.79.3:${WLS_PORT:-8410}/api/health
volumes:
waterland_studio_data: {}
waterland_studio_kernels: {}
networks:
tnet:
name: traefik-net
external: true