Build and Push Container / build-and-push (push) Successful in 38s
Single-file FastAPI proxy plus a no-CDN vanilla-JS page for watching a gufo
server and a vLLM server side by side: one Prometheus parser (class Prom) with
per-engine field maps, measured Δcounter/Δt rates for engines that export no
rate gauge, and strict read-only GET-only routing. Engines are opt-in via
*_BASE_URL — unset means no panel, no poll, and a 404 {"configured": false}
without dialling.
Includes the container image and CI so the dashboard can be deployed as an
image rather than a checkout:
- Containerfile: uv-locked two-stage build on python:3.14-slim, non-root
(uid 10001), binds 8090 (8000 is vLLM's port in this fleet), healthcheck on
/api/config so a dead engine never marks the dashboard unhealthy. No frontend
build stage — static/index.html is bundler-free by design and is copied as-is.
- .gitea/workflows/build-and-push.yml: publishes
gitea.reeseapps.com/Experimental/llm-monitor on main + release tags.
114 tests, no network and no real engines required.
LLM-Generated: true
60 lines
2.8 KiB
Docker
60 lines
2.8 KiB
Docker
# 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"]
|