# Hold Slayer — single image: FastAPI process that also serves the built # SvelteKit dashboard at "/". One container, four surfaces (REST/WS/MCP/dash). # # pjsua2 is deliberately NOT built here — it is not pip-installable (compiled # from pjproject) and the media pipeline degrades to documented stub mode # without it. That is correct for a mock-SIP dev deploy (USE_MOCK_SIP=true); # /health honestly reports the mock engine as "degraded". Building real media # is a separate, deliberate piece of work. # Stage 1: build the SvelteKit dashboard → dashboard/build/ (SPA, static). # dashboard/build and dashboard/node_modules are gitignored, so build fresh # here rather than copying a stale working-tree artifact. FROM node:22-alpine AS dashboard WORKDIR /dashboard COPY dashboard/package.json dashboard/package-lock.json ./ RUN npm ci COPY dashboard/ ./ RUN npm run build # Stage 2: runtime. The app runs FROM SOURCE at /app (not purely from # site-packages): db/database.py locates alembic.ini via # Path(__file__).parent.parent, and main.py/config.py are loose top-level # modules — both require the source tree layout under the working dir. An # editable install puts the deps + entry points in place while keeping /app/db, # /app/config.py, /app/alembic.ini resolving to the real files. FROM python:3.12-slim WORKDIR /app # build-essential: some deps compile from source (no manylinux wheel). # curl: required for the compose healthcheck (GET /health). RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential curl \ && rm -rf /var/lib/apt/lists/* # Install dependencies first (better layer caching) using just the manifest, # then the source. -e keeps the package importable from /app so alembic.ini # and the loose modules resolve correctly at runtime. COPY pyproject.toml README.md ./ COPY . . # Bring in the freshly built dashboard (overwrites any stale gitignored copy). COPY --from=dashboard /dashboard/build ./dashboard/build RUN pip install --no-cache-dir -e . \ && apt-get purge -y build-essential && apt-get autoremove -y EXPOSE 21081 # Structured logs by default in the container: the host's Alloy agent reads # stdout and ships it to Loki, where text lines arrive as an unqueryable blob. # Overridable (LOG_FORMAT=text) for interactive `docker run` debugging. ENV LOG_FORMAT=json # Migrations run in the app's own init_db() on boot (db/database.py), so no # separate `alembic upgrade` here. Bind host/port from the same env vars # pydantic-settings reads (HOST/PORT) so configured values and the actual bind # cannot drift. Deploy sets PORT=21081 (image default 8000 collides with other # host-net services on triton). CMD ["sh", "-c", "uvicorn main:app --host ${HOST:-0.0.0.0} --port ${PORT:-21081}"]