Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Phase 6: Performance & Production Hardening - Context
Gathered: 2026-05-30 Status: Ready for planning
## Phase BoundaryThe application is hardened and observable for production deployment. This phase delivers: structured JSON logging with correlation IDs and a Loki+Grafana aggregation stack in Docker Compose; a Locust load test suite with defined SLA targets (p95 < 200ms, p99 < 500ms) against the auth + document CRUD endpoints; container hardening via multi-stage Dockerfile with non-root appuser, read-only root filesystem with tmpfs mounts, and ALL Linux capabilities dropped; rate limit header-bypass prevention via a custom trusted-proxy IP extraction function; per-account rate limits on authenticated endpoints; and a RUNBOOK.md documenting all env vars, startup/shutdown, backup strategy, and on-call escalation.
No new user-facing features. All changes are operational and security hardening.
## Implementation DecisionsObservability — Structured Logging
- D-01: Use
structlogfor structured JSON logging. Configure a processors pipeline that injects correlation IDs, user_id, request latency, and HTTP method/path into every log line. A FastAPI middleware generates a UUID correlation ID per request and binds it into the structlog context. - D-02: All services emit JSON to stdout. Loki + Grafana are added as services in
docker-compose.yml(Loki as log storage, Grafana as query UI). Promtail or the Docker log driver ships logs from the backend container to Loki. - D-03: No distributed tracing (OpenTelemetry skipped). Correlation IDs in structured logs are sufficient for request tracing at this scale.
Load Testing
- D-04: Use Locust for load testing. Test scenarios written in Python at
backend/load_tests/locustfile.py. Locust can be run headless (locust --headless) or with its web UI (locust --host=http://localhost:8000). - D-05: Load test scope: login → list documents → get a document → upload a document. Simulates a realistic user session. Cloud backend endpoints excluded (external provider latency would invalidate local SLA targets).
- D-06: SLA targets:
- p50 < 100ms, p95 < 200ms, p99 < 500ms on all covered endpoints
- Test parameters: 50 concurrent users, 5-minute soak (matches ROADMAP.md success criteria SC-01)
- Load test passes when zero endpoint failures AND all p95/p99 targets met
Container Hardening
- D-07: Multi-stage Dockerfile:
builderstage installs Python dependencies and system packages as root;runtimestage copies only the installed packages and app code, createsappuser(uid 1000), and setsUSER appuser. System deps (tesseract-ocr, libgl1, libglib2.0-0) installed in the runtime stage since they are runtime requirements. - D-08: Read-only root filesystem: Add
read_only: trueto the FastAPI and Celery worker services indocker-compose.yml. Addtmpfs: ["/tmp"]for temporary file operations (PyMuPDF temp files, Celery task temp downloads). The/app/datapath is a named volume (bind mount) that remains writable for application data. - D-09: Dropped capabilities:
cap_drop: [ALL]on both backend services. Nocap_add— port 8000 is unprivileged and requires no capabilities. - D-10:
docker scoutCVE scan: Rundocker scout cveson the built image as part of the security gate. Zero critical CVEs required before phase is marked complete.
Rate Limiting — Header Bypass Prevention
- D-11: Replace
get_remote_address(the default slowapi key function) with a customget_client_ip(request)function. Logic:- If
request.client.hostis in a trusted proxy CIDR (127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1), read the leftmost IP fromX-Forwarded-For. - Otherwise, use
request.client.hostdirectly — ignore all forwarded headers. This prevents header spoofing from external clients while preserving correct behavior when a legitimate reverse proxy is in front.
- If
- D-12: Add per-account rate limits on authenticated endpoints in addition to the existing per-IP limits. Use a second
Limiterinstance keyed bycurrent_user.id(injected via a dependency). Target limits: 100 req/min per authenticated user on document/cloud endpoints; existing auth endpoint limits (10/min IP, 5/hour for password reset) remain unchanged. - D-13: Existing per-IP limits on auth endpoints (
@limiter.limit("10/minute"),@limiter.limit("5/hour")) are preserved and strengthened only by switching to the trusted-proxy key function.
Runbook
- D-14:
RUNBOOK.mdat repo root. Contents: all required env vars with descriptions and examples; Docker Compose startup/shutdown procedures; backup strategy for PostgreSQL (pg_dump cron) and MinIO (mc mirror); health check verification steps; on-call escalation path (who to contact, in what order, for which alert types); common failure modes and recovery steps.
Claude's Discretion
- Exact structlog processor chain configuration (which fields, which order) — follow structlog documentation best practices.
- Loki Docker Compose service version and configuration (loki-config.yaml) — use the official Grafana Loki Docker Compose example as the base.
- Promtail vs. Docker log driver for shipping logs to Loki — Claude picks based on simplicity.
- Locust user class structure and task weight distribution.
- Specific Grafana dashboard panel layout — basic request rate + latency + error rate panels are sufficient.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Phase Goal and Success Criteria
.planning/ROADMAP.md§"Phase 6: Performance & Production Hardening" — Goal, success criteria (SC-01 through SC-05), and phase gates. Requirements are TBD in ROADMAP.md but captured fully in this CONTEXT.md.
Security Mandates (Non-Negotiable)
CLAUDE.md§"Key Architectural Rules" — JWT memory-only, refresh httpOnly cookie, atomic quota UPDATE, admin endpoint restrictions.CLAUDE.md§"Security Protocol" — Container hardening checklist (non-root, read-only fs, dropped caps,docker scout), bandit/pip audit/npm audit gates, no hardcoded secrets.CLAUDE.md§"Security Requirements" — Rate limiting on all auth endpoints, constant-time comparison, CSRF protection.
Existing Rate Limiting Code
backend/api/auth.pylines 37–44 — currentLimiter(key_func=get_remote_address)setup; replaceget_remote_addresswith custom trusted-proxy function.backend/main.pylines 9–16, 108–110 — SlowAPIMiddleware registration and limiter state attachment.
Container Configuration
backend/Dockerfile— current single-stage build running as root; must be replaced with multi-stage + appuser pattern.docker-compose.yml— addread_only,tmpfs,cap_dropto backend service; add Loki + Grafana services.
Testing Infrastructure
backend/tests/conftest.py— existing async test fixtures and auth helpers; Locust scenarios should reuse the same auth patterns.backend/pytest.ini— test runner config; load tests live separately inbackend/load_tests/and are NOT run bypytest -v.
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
backend/api/auth.py:37–44—Limiter+get_remote_addresssetup; extend to per-account limiter by adding a secondLimiter(key_func=lambda req: str(current_user.id))pattern.backend/main.py:108–110— SlowAPIMiddleware already wired; adding a correlation ID middleware follows the sameapp.add_middleware()pattern.backend/tests/conftest.py— auth fixtures (auth_client,admin_client) that Locust user classes can adapt to Python-based login flows.
Established Patterns
asyncio.to_thread()— all sync SDK calls already wrapped (MinIO, cloud backends); log emission is sync-safe so structlog integrates cleanly.get_regular_user/get_current_admindependency chain — per-account rate limiter should extractuser_idfrom the samecurrent_userobject already injected by these deps.- Pydantic Settings (
backend/config.py) — new env vars (trusted proxy CIDRs, structlog level, Loki endpoint) added viaSettingsclass following the existing pattern.
Integration Points
backend/main.py— add correlation ID middleware, wire per-account limiter state.docker-compose.yml— add Loki + Grafana services; addread_only: true,tmpfs,cap_dropto backend and celery-worker services.backend/Dockerfile— replace with multi-stage build.backend/api/auth.py— replaceget_remote_addresswith customget_client_ip.backend/api/documents.py,backend/api/cloud.py— add per-account rate limit decorators.
</code_context>
## Specific Ideas- Loki stack: Use the official Grafana
docker-composeexample for Loki + Grafana (single-binary Loki mode is sufficient for local dev). Promtail or Docker log driver picks up container stdout. - Locust location:
backend/load_tests/locustfile.py— separate directory fromtests/sopytestdoes not discover it. Run vialocust --headless --users 50 --spawn-rate 10 --run-time 5m --host http://localhost:8000. - Correlation ID middleware: Generate
str(uuid.uuid4())per request, bind to structlog context viastructlog.contextvars.bind_contextvars(correlation_id=...), include in response asX-Correlation-IDheader. - RUNBOOK.md location: Repo root alongside CLAUDE.md and README.md.
- HTTPS/TLS termination — adding nginx + Let's Encrypt or Caddy in front of the stack. Out of scope for Phase 6; the runbook documents how to add a reverse proxy.
- Horizontal scaling — multiple uvicorn workers, Redis-backed rate limit counters, sticky sessions. Currently in-memory rate limits suffice for single-instance deployment. Phase 7+ concern.
- CI/CD pipeline — GitHub Actions workflow for automated load tests and
docker scouton every PR. Out of scope for Phase 6 (no CI setup exists yet). - Backup automation — automated pg_dump + MinIO mirror cron job as a Docker service. RUNBOOK.md documents the manual procedure; automation is a future operational phase.
Phase: 6-Performance & Production Hardening Context gathered: 2026-05-30