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
191 lines
5.4 KiB
YAML
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
|