services: api: build: context: . dockerfile: api/Dockerfile restart: unless-stopped ports: - "127.0.0.1:8080:8080" env_file: - .env environment: - TZ=America/New_York volumes: # Mount the whole api/ source so uvicorn --reload picks up .py edits live — # no docker cp, no image drift. Nested mounts below layer on top. - ./api:/app - ./api/data:/app/data - ./api/templates:/app/templates - ./web:/app/static # App submodules (spire, noodle): api/app_paths.py puts apps/*/api on # sys.path; their web/ joins the "/" static mount, their templates _tmpl. - ./apps:/app/apps - ./graphify-out:/app/graphify-out:ro - ./mtg/data:/app/mtg/data - ./nightfall-incident:/app/nightfall # KEPT ON PURPOSE though the rmapi binary is gone (2026-09-17). This volume # holds a real authenticated reMarkable device token (`rmapi.conf`, last # written 2026-04-28) -- re-pairing a device is a manual, physical act, so # the token is not something to throw away to tidy a compose file. Left # MOUNTED rather than merely declared, because a declared-but-unmounted # compose volume counts as unused and `docker volume prune` would eat it. # Costs ~1.5KB. Restore the feature by putting the builder stage back. - rmapi-auth:/root/.config/rmapi - gcal-auth:/root/.config/gcal # nightfall-src/ is the GAME'S SOURCE tree (354MB, 29k entries incl. node_modules). # Nothing at runtime reads it -- only the BUILT output is served (index.html, # static/, wai-*.js, audio/), and webpack runs on the HOST, never in here. But it # rode along inside the ./nightfall-incident bind mount, and uvicorn --reload # rglob()s the whole /app tree looking for *.py, so those 29k entries were 85% of # a walk that ran 4x a second: 0.21s per pass on a 0.25s interval = 44% of a core, # permanently, for 8 days straight. An empty tmpfs over that one path masks it out # of the container (34,187 entries walked -> 5,177) and costs 0 bytes, since # nothing ever writes there. Docker mounts shallowest-first, so this lands ON TOP # of the ./nightfall-incident bind above it; the host copy is untouched. tmpfs: - /app/nightfall/nightfall-src # The docker log had no size limit and lived as long as the container. It is # capped (3 x 10MB, oldest dropped) -- the access lines in it carried noodle # slugs and draft tokens until api/log_masking.py (security audit 2026-10-14). logging: driver: json-file options: max-size: "10m" max-file: "3" # Always-on GLaDOS voice (CPU Piper). Built separately from the hosaka repo: # docker build -f ~/src/hosaka/Dockerfile.piper -t hosaka-piper:latest ~/src/hosaka # No torch, glados baked into the image. Keeps /hosaka's glados live even when # the home GPU box is off. The api container reaches it by service name over # the compose network (TTS_PIPER_UPSTREAM defaults to hosaka-piper:8123); no # ports are published -- only api talks to it. hosaka-piper: image: hosaka-piper:latest restart: unless-stopped volumes: # Pronunciation lexicon synced from the home box (scripts/sync_lexicon.sh, # rsync --inplace). Single-file bind mount + hosaka's Lexicon mtime # hot-reload => live pronunciation updates with no restart. The host file # must exist before the container starts (else docker creates a dir); # seed it with the first sync. Read-only: the container only reads it. - /srv/hosaka-piper/lexicon.json:/opt/hosaka/.local/share/hosaka/lexicon.json:ro volumes: rmapi-auth: gcal-auth: