Skip to content

06. Infra & Local Ops

Local development topology

graph TD
  classDef default fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
  classDef highlight fill:#065f46,stroke:#34d399,stroke-width:2px,color:#f0fdf4
  classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fffbeb

  Dev["Developer machine"]:::highlight
  FE["Vite :5173"]
  API["Uvicorn :8000"]
  CW["Celery worker"]
  RD["Redis :6380<br/>passworded"]
  Jobs["./jobs"]
  Paddle["Optional Paddle VL<br/>:1234"]:::warning

  Dev --> FE
  FE --> API
  API --> RD
  CW --> RD
  API --> Jobs
  CW --> Jobs
  CW --> Paddle

  linkStyle default stroke:#64748b,stroke-width:2px

Make targets (high signal)

From project Makefile:

Target Effect
make setup uv sync + frontend pnpm install
make up / down / nuke Redis via docker-compose.yml
make backend-start FastAPI + Celery in background with PID files
make frontend-start Vite on FRONTEND_PORT (5173)
make start / stop / restart Full local stack
make test pytest
make fmt black + isort
make docs-serve Project MkDocs on :8001
make health Health check helper

Prereqs: uv, pnpm, Docker for Redis.

Compose files

docker-compose.yml (infra only)

  • Redis 7 Alpine with --requirepass, AOF, host port ${REDIS_PORT:-6380}
  • Volume ./volumes/redis_data
  • Healthcheck uses password

docker-compose-full.yml (API + worker + Redis)

  • Redis on 6379 without password
  • backend and worker images, platform: linux/amd64
  • Shared ./volumes/jobs/app/jobs
  • REDIS_URL=redis://redis:6379/0 plus .env file

Confirmed footgun: local compose expects passworded Redis on 6380; full compose uses open Redis on 6379. .env REDIS_URL must match the stack you start.

Docker images

File Base CMD
Dockerfile.backend ghcr.io/astral-sh/uv:python3.13-bookworm-slim uvicorn app.main:app --host 0.0.0.0 --port 8000
Dockerfile.celery same celery ... worker --queues ocr --pool solo

Both install OpenCV/Paddle system libs (libgl1, libglib2.0-0, libgomp1) and uv sync --frozen.

Apple Silicon note from README.md: enable Rosetta for amd64 emulation; Paddle lacks official Linux ARM64 wheels.

Environment variables

See .env.example. Critical groups:

  • Redis connectivity
  • VLM_MODE / VLM_MODEL / Paddle server URL
  • Provider API keys (NVIDIA NIM for LiteLLM, NVIDIA OCR, Sarvam)
  • Concurrency and timeouts
  • JOBS_DIR, PORT, HOST, ALLOWED_HOSTS
  • CELERY_CONCUR (Makefile-oriented; Docker worker uses solo pool)

Networking and ports

Service Default port
FastAPI 8000
Vite 5173
Redis (local compose) 6380 → 6379
Redis (full compose) 6379
Project docs 8001
Paddle VL server (example) 1234

Startup / health

  • API lifespan pings Redis and optionally prewarms models
  • GET /health{"status":"ok","service":"universal-ocr"}
  • Celery prewarms on worker process init when enabled

Secrets handling

  • Secrets expected in .env (gitignored pattern in project)
  • Example file documents keys but leaves values empty
  • Redis password only in local compose path

Observability

  • Stdout/file logs via Makefile PID + logs/ directory conventions
  • No OpenTelemetry / Prometheus instrumentation found in inspected code
  • Progress observability is product-facing via Redis status fields

Infra interview Q&A

Q: How do you run this on a Mac with Apple Silicon?
A: Prefer local make start with Redis container, or full compose under linux/amd64 + Rosetta; Paddle wheels drive the amd64 requirement.

Q: How do API and worker share artifacts?
A: Same JOBS_DIR / bind-mounted volume so the API can serve files the worker wrote.

Q: Why solo pool?
A: Heavy OCR + native model state; solo avoids fork issues with Paddle/OpenCV in the worker process (Inferred from Docker CMD and model singleton design).