# syntax=docker/dockerfile:1 # llm_monitor — production image (Podman/Docker compatible). # # Stage 1 (python): install the locked dependencies with uv (deps only — the app is # a single module, there is nothing to build or install). # Stage 2 (runtime): slim, non-root, no build tools. # # There is deliberately NO frontend build stage: static/index.html is hand-rolled vanilla # JS + inline SVG with no bundler and no CDN (the engines live on an internet-disconnected # podman network, so a CDN asset would be a dead request and a blank chart). The file is # copied verbatim and served as-is. # # podman build -t localhost/llm-monitor:main -f Containerfile . # podman run -d --name llm-monitor -p 8090:8090 \ # -e GUFO_BASE_URL=http://deskwork-ai:8731 \ # -e VLLM_BASE_URL=http://driveripper-ai:8000 \ # localhost/llm-monitor:main # # Both env vars are optional — unset means this deployment does not run that engine, the # proxy answers 404 {"configured": false} without dialling, and the page drops the panel. # Nothing is baked into the image: no host guesses, no keys. # # Both stages must stay on the SAME Python (3.14): stage 1 builds the venv that stage 2 # copies verbatim, so a mismatch would ship a venv the runtime interpreter cannot import. # requires-python is >=3.10; 3.14 is what the box that runs this is on. # ---------- Stage 1: python dependencies ---------- FROM docker.io/python:3.14-slim AS python WORKDIR /app COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv ENV UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=0 \ UV_PROJECT_ENVIRONMENT=/app/.venv # Lockfile first so dependency layers cache independently of the app source. COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev --no-cache # ---------- Stage 2: runtime ---------- FROM docker.io/python:3.14-slim AS runtime ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 RUN useradd --create-home --uid 10001 monitor WORKDIR /app COPY --from=python /app/.venv /app/.venv COPY app.py ./app.py COPY static ./static RUN chown -R monitor:monitor /app USER monitor # The dashboard listens on 8090 on purpose: 8000 is vLLM's port in this fleet and 8731 is # gufo's, so binding either would collide with an engine (or scrape itself). EXPOSE 8090 ENV DASHBOARD_HOST=0.0.0.0 \ DASHBOARD_PORT=8090 # /api/config answers 200 with no upstream configured and never dials an engine, so a dead # gufo or vLLM can never make the dashboard container unhealthy. HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD ["python", "-c", "import sys, httpx; sys.exit(0 if httpx.get('http://127.0.0.1:8090/api/config', timeout=4).status_code == 200 else 1)"] CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"]