Docker Lab — Hardened and Observable LLMOps API
This lab packages a deterministic FastAPI application as an operational control. It requires no API keys and does not call an LLM: this allows you to isolate build, health, signals, metrics, and container security before
This lab packages a deterministic FastAPI application as an operational control. It requires no API keys and does not call an LLM: this allows you to isolate build, health, signals, metrics, and container security before adding an external dependency.
What it demonstrates#
- multi-stage build with
uv 0.12.5and pinned dependencies (catalog dated 2026-08-21); - slim runtime without compiler, UID/GID 10001 process, and
CMDexec; - separate health/readiness checks with no paid API calls;
- read-only filesystem, dropped capabilities, and
no-new-privilegesin Compose; - Prometheus metrics without user labels or prompt data;
- Prometheus image pinned to
v3.14.0and 24-hour local retention.
Periodically revalidate versions and digests; a pinned tag reduces drift, but does not replace a scanner or an update policy.
Run#
From this directory:
docker compose config --quiet
docker compose build
docker compose up -d --wait
curl -fsS http://127.0.0.1:8000/health
curl -fsS http://127.0.0.1:8000/ready
curl -fsS -X POST http://127.0.0.1:8000/v1/answer \
-H 'content-type: application/json' \
-d '{"question":"He expuesto una clave API, ¿qué hago?"}'
curl -fsS http://127.0.0.1:8000/metrics | head
Prometheus is available at http://127.0.0.1:9090. Query:
sum by (endpoint, status) (rate(llmops_requests_total[1m]))
To stop without deleting the metrics volume:
docker compose down
Reading the Dockerfile#
uv-binsets the build tool.buildercreates/opt/venv; copyingrequirements.txtfirst preserves the cache.runtimereceives only Python, the venv, andapp.py.- The non-root user has no shell or writable home directory.
- The health check uses the stdlib; it does not install
curlat runtime. - Uvicorn is PID 1 and receives SIGTERM directly.
Acceptance tests#
docker compose ps
docker inspect --format '{{.Config.User}} {{.HostConfig.ReadonlyRootfs}}' llmops-docker-api-1
docker compose exec api python -c 'import os; print(os.getuid())'
docker compose exec api sh -c 'touch /app/forbidden'
The UID must be 10001, ReadonlyRootfs must be true, and the last command must fail. The generated container name may vary: docker compose ps -q api returns its ID if you need a portable command.
Evolution towards a real API#
Inject keys from the orchestrator's secret manager, add an async client with timeout, circuit breaker, and budget, and keep /health provider-free. Never add prompt or user ID as a Prometheus label: this creates unbounded cardinality and leaks data. Detailed traces belong to a tracing backend with defined authoring and retention policies.