pdf-quiz-generator/docker-compose.yml
Daniel af59fdb960 fix: MinIO name collision, and refuse to write an article from fragments
MinIO was resolving to the wrong container
Putting the backend on danvics_milvus to reach the clinical index gave it a
second service called `minio`, and Docker resolved that one first. Every object
read failed with InvalidAccessKeyId while the bucket simply looked empty — all
435 stem images unservable, and nothing in the logs saying why. The quiz MinIO
now answers to `quiz-minio`, which nothing else on this host claims.

A topic named after a shelf retrieved headings, not prose
"Pediatric Pulmonology" returned ten chunks whose top hit was 29 characters —
`**270** Pediatric Pulmonology`, an index line. Chapter titles rank well against
a query that looks like a chapter title. The model was handed a prompt with
citations and no content and said so, which was the correct response and read as
a JSON failure.

Two gates, both stated in the code. A chunk under 200 characters is a heading or
a running header rather than something to write from. A topic whose passages
total under 3,000 characters is skipped with the count in the reason, rather than
asking a model to write a medical article out of fragments — it will either
refuse or invent, and only one of those is visible.

The 71 generated drafts are deleted at the user's request. Nothing linked to
them and generation is resumable, so the cost was model calls rather than work.

Question bank corrections, from the agent that ran alongside:
262 questions had OCR-mangled units repaired — `inEq/L`, `mrnol/L`, flattened
`10⁹` superscripts and the rest — each with a version snapshot written first, so
every edit is reversible from the existing question editor. 94 stem images that
belonged to the explanation were removed; PREP's own `Item Q37A` / `Item C37B`
labels turned out to be a far better signal than word cues, taking the confident
split from 69/58/308 to 300/81/54. 13 uncertain images are listed for a person.

203 backend, 223 frontend green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-10 17:31:53 +02:00

191 lines
5.4 KiB
YAML

services:
postgres:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_DB: pedquiz
POSTGRES_USER: pedquiz
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pedquiz"]
interval: 5s
timeout: 5s
retries: 10
frontend:
build: ./frontend
env_file:
- ./frontend/.env
ports:
- "127.0.0.1:8081:80"
depends_on:
- backend
restart: unless-stopped
backend:
build: ./backend
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
env_file:
- ./backend/.env
environment:
- ANONYMIZED_TELEMETRY=False
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
- uploads_data:/app/uploads
- chroma_data:/app/chroma_data
networks:
- default
- danvics_speech
- danvics_milvus
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
celery:
build: ./backend
# --beat runs the embedded scheduler (single worker, so no lock needed).
command: celery -A app.tasks worker --beat --loglevel=${LOG_LEVEL:-info} --concurrency=2
env_file:
- ./backend/.env
environment:
- ANONYMIZED_TELEMETRY=False
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
- uploads_data:/app/uploads
- chroma_data:/app/chroma_data
networks:
- default
- danvics_milvus
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
quiz-telegram-bot:
build: ./telegram-bot
env_file:
- ./telegram-bot/.env
environment:
- DATABASE_URL=postgresql://pedquiz:${POSTGRES_PASSWORD}@postgres:5432/pedquiz
- PUBLIC_APP_URL=${APP_URL:-https://pedshub.com}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
# ── Logging: Loki + Promtail + Grafana ──────────────────────────────
loki:
image: grafana/loki:3.3.2
restart: unless-stopped
command: -config.file=/etc/loki/loki-config.yml
volumes:
- ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro
- loki_data:/loki
ports:
- "127.0.0.1:3100:3100"
promtail:
image: grafana/promtail:3.3.2
restart: unless-stopped
command: -config.file=/etc/promtail/promtail-config.yml
volumes:
- ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- promtail_positions:/positions
depends_on:
- loki
grafana:
image: grafana/grafana:10.3.1
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD}
GF_AUTH_ANONYMOUS_ENABLED: "false"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
ports:
- "127.0.0.1:3002:3000"
depends_on:
- loki
db-backup:
image: prodrigestivill/postgres-backup-local:16
restart: unless-stopped
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: pedquiz
POSTGRES_USER: pedquiz
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
SCHEDULE: "@daily"
BACKUP_KEEP_DAYS: 14
BACKUP_KEEP_WEEKS: 4
BACKUP_KEEP_MONTHS: 6
HEALTHCHECK_PORT: 8080
volumes:
- ./backups:/backups
depends_on:
postgres:
condition: service_healthy
# Object storage for media. Files this size do not belong in a container
# volume that only one host can mount, and S3 semantics give presigned URLs
# and lifecycle rules that a bind mount cannot.
minio:
image: minio/minio:RELEASE.2024-10-13T13-34-11Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-pedshub}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
networks:
default:
# An unambiguous name. The backend also sits on danvics_milvus, which
# has a MinIO of its own called `minio`, and Docker resolved that one
# first — every object read failed with InvalidAccessKeyId while the
# bucket looked simply empty.
aliases:
- quiz-minio
# No host ports: the backend reaches MinIO over the compose network, and
# 9000/9001 are already taken on this host. Publish deliberately if the
# console is ever needed from outside.
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 20s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
uploads_data:
minio_data:
chroma_data:
postgres_data:
redis_data:
loki_data:
grafana_data:
promtail_positions:
networks:
danvics_speech:
external: true
# The clinical library index. Note there are two Milvus servers on this host:
# this is the one holding mcp_bge_m3_1024, reached as `milvus`. The other, on
# ped-ai-storage_basic, is a different instance with different credentials.
danvics_milvus:
external: true