# waterland studio — GPU watercolour render service. # # Build context is a CHECKOUT OF vh/waterland, not this directory. See # compose.yaml: context is /opt/waterland-studio/src and this Dockerfile is # passed out-of-context so `deploy-stack.sh --delete` can never eat the # checkout. Refresh the checkout with ./update.sh. FROM python:3.12-slim # ffmpeg is not optional — the CLI shells out to it for the VP9 encode of the # reveal animation. Without it, plate renders succeed and animated ones fail # at the very end of a 30s GPU job. RUN apt-get update \ && apt-get install -y --no-install-recommends ffmpeg ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /usr/local/bin/uv WORKDIR /app COPY . /app # ⚠️ BOTH extras are load-bearing. `gpu` carries cupy-cuda12x; a bare # `uv sync` PRUNES it and the renderer silently drops to the numpy path at # roughly 21x the wall time — it does not error, it just gets slow. `studio` # carries fastapi/uvicorn/python-multipart. RUN uv sync --frozen --extra studio --extra gpu # ⚠️ CUDA HEADERS — the dependency the host never had to declare. # cupy compiles kernels at runtime through NVRTC, which needs the CUDA toolkit # HEADERS present, not just the driver and the runtime libs bundled in the # cupy-cuda12x wheel. On irv-ml1 that requirement was invisible: a CUDA toolkit # is installed system-wide, so the bare `nohup` process found headers by # accident. In a slim image there are none, and every render dies 1.7s in with # RuntimeError: Failed to find CUDA headers. # printed through argparse's usage banner, which makes it read like a CLI # argument bug rather than a missing toolkit. # # The [ctk] extra pulls the header packages as wheels — a few hundred MB # against ~6 GB for a -devel base image. It is installed AFTER the sync above # because `uv sync` prunes anything it does not know about. RUN uv pip install "cupy-cuda12x[ctk]" ENV PATH="/app/.venv/bin:${PATH}" \ # ⚠️ THE SAME PRUNE TRAP, AT RUNTIME. studio/jobs.py shells the renderer # out as a literal `uv run waterland ...` (cwd=WATERLAND_STUDIO_REPO), and # that invocation carries no --extra flags. Left to itself uv would # re-sync the project to its default extras and prune cupy right back out # from under the venv built above. UV_NO_SYNC stops it re-syncing; # UV_OFFLINE means that if the pin ever stops working the job fails LOUDLY # instead of quietly rebuilding a slower environment. UV_NO_SYNC=1 \ UV_OFFLINE=1 \ WATERLAND_STUDIO_REPO=/app \ WATERLAND_STUDIO_DATA=/data \ WATERLAND_STUDIO_BACKEND=cupy \ # PCI_BUS_ID index of the A6000 *as seen inside the container*. The # compose file exposes exactly one GPU, so that GPU is index 0 here — even # though it is index 1 on the host. Do not copy the host's value. CUDA_VISIBLE_DEVICES_TARGET=0 EXPOSE 8410 # uvicorn is invoked from the venv directly rather than through `uv run`: the # server has no reason to re-enter uv, and one less uv invocation is one less # chance to trip the prune above. The app is a FACTORY, hence --factory. CMD ["uvicorn", "--factory", "waterland.studio.app:app", \ "--host", "0.0.0.0", "--port", "8410"]