| # Face Intel — Deployment Guide |
|
|
| This guide covers local development setup, Docker packaging, |
| environment configuration, database setup, storage layout, reverse |
| proxy integration, and load balancer health probes. |
|
|
| > **No Dockerfile exists yet** in the repository. This document |
| > describes the recommended Docker layout — implementing it is |
| > tracked as a follow-up task (see [§13](#13-follow-ups)). |
|
|
| --- |
|
|
| ## Table of Contents |
|
|
| 1. [Prerequisites](#1-prerequisites) |
| 2. [Local Development Setup](#2-local-development-setup) |
| 3. [Running the Server](#3-running-the-server) |
| 4. [Environment Variable Configuration](#4-environment-variable-configuration) |
| 5. [Database Setup](#5-database-setup) |
| 6. [Storage Directory Layout](#6-storage-directory-layout) |
| 7. [Docker Deployment](#7-docker-deployment) |
| 8. [Kubernetes Deployment](#8-kubernetes-deployment) |
| 9. [Reverse Proxy Considerations](#9-reverse-proxy-considerations) |
| 10. [Health Check Endpoints for Load Balancers](#10-health-check-endpoints-for-load-balancers) |
| 11. [TLS / HTTPS](#11-tls--https) |
| 12. [Operational Runbook](#12-operational-runbook) |
| 13. [Follow-ups](#13-follow-ups) |
|
|
| --- |
|
|
| ## 1. Prerequisites |
|
|
| ### Runtime |
|
|
| - **Python 3.11+** (3.12 recommended). |
| - **pip** and **venv**. |
| - **OpenCV system dependencies** (Linux): `libgl1`, `libglib2.0-0`. |
| On Debian/Ubuntu: `apt install -y libgl1 libglib2.0-0`. |
| - **Chrome/Chromium** (only if you enable `selenium_scraper` or |
| `google_lens`): `google-chrome` or `chromium-browser`. |
|
|
| ### Python dependencies |
|
|
| The full list is in [`requirements.txt`](../requirements.txt). Key |
| packages: |
|
|
| | Package | Used by | |
| |---|---| |
| | `fastapi`, `uvicorn`, `pydantic`, `pydantic-settings` | API layer | |
| | `opencv-python`, `Pillow`, `numpy` | Image processing | |
| | `face-recognition`, `dlib` | Recognition (optional) | |
| | `mtcnn`, `tensorflow` | Detection (optional) | |
| | `beautifulsoup4`, `lxml`, `requests` | Scraping | |
| | `selenium`, `webdriver-manager` | JS-rendered scraping | |
| | `loguru` | Structured logging | |
| | `pytest`, `pytest-asyncio` | Test runner | |
|
|
| ### Hardware |
|
|
| - **CPU-only:** Haar + DNN + image_analysis + forensics work well. |
| DNN inference: ~30-80 ms per image. |
| - **GPU (optional):** DNN auto-detects CUDA. InsightFace and |
| DeepFace benefit substantially. Requires `opencv-python-headless` |
| built with CUDA or the `onnxruntime-gpu` package. |
| - **RAM:** 1 GB minimum for dev. 4 GB recommended for production with |
| MTCNN/InsightFace enabled. |
| - **Disk:** ~50 MB for the DNN model, ~550 MB for InsightFace |
| `buffalo_l` pack, ~1 GB for `data/` over a year of jobs. |
|
|
| --- |
|
|
| ## 2. Local Development Setup |
|
|
| ### Step 1 — Clone and create a virtualenv |
|
|
| ```bash |
| git clone <repo-url> face-intel |
| cd face-intel |
| python3.11 -m venv .venv |
| source .venv/bin/activate |
| ``` |
|
|
| ### Step 2 — Install dependencies |
|
|
| ```bash |
| pip install --upgrade pip |
| pip install -r requirements.txt |
| ``` |
|
|
| > **dlib compilation:** `dlib==19.24.2` requires CMake and a C++ |
| > compiler. On Debian/Ubuntu: `apt install -y build-essential cmake`. |
| > Compilation takes ~5 minutes. See |
| > [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md) if it fails. |
|
|
| ### Step 3 — Configure |
|
|
| ```bash |
| cp .env.example .env |
| # Edit .env to enable/disable providers |
| ``` |
|
|
| At minimum, review: |
|
|
| - `FI_ENABLE_*` flags (which providers to load). |
| - `FI_SERPAPI_KEY`, `FI_BING_API_KEY`, `FI_TINEYE_PUBLIC_KEY`, |
| `FI_TINEYE_PRIVATE_KEY` (only if you enable the paid providers). |
| - `FI_LOG_LEVEL`, `FI_DEBUG` for verbosity. |
|
|
| See [`docs/CONFIGURATION.md`](CONFIGURATION.md) for the full table. |
|
|
| ### Step 4 — Verify installation |
|
|
| ```bash |
| # Verify imports don't have circular deps |
| python scripts/check_imports.py |
| |
| # Verify settings load |
| python -c "from config.settings import settings; print(settings.model_dump_json(indent=2))" |
| |
| # Run the test suite (should be 145 passing) |
| python -m pytest tests/ -v |
| ``` |
|
|
| --- |
|
|
| ## 3. Running the Server |
|
|
| ### Option A — Direct |
|
|
| ```bash |
| python app.py |
| ``` |
|
|
| Reads `config/settings.py::settings` and runs uvicorn with reload if |
| `FI_DEBUG=true`. |
|
|
| ### Option B — uvicorn directly |
|
|
| ```bash |
| uvicorn app:app --host 0.0.0.0 --port 8000 --reload |
| ``` |
|
|
| Useful for development — `--reload` watches for file changes. |
|
|
| ### Option C — production-style |
|
|
| ```bash |
| uvicorn app:app --host 0.0.0.0 --port 8000 \ |
| --workers 4 --no-access-log --log-level info |
| ``` |
|
|
| - `--workers 4`: run 4 worker processes (each with its own in-memory |
| cache — see [§9](#9-reverse-proxy-considerations)). |
| - `--no-access-log`: silence per-request access logs (useful when |
| structured logging is enabled). |
|
|
| ### Verifying it's up |
|
|
| ```bash |
| curl http://localhost:8000/health |
| # → {"status":"ok"} |
| |
| curl http://localhost:8000/providers | jq '.providers | length' |
| # → 24 |
| |
| curl http://localhost:8000/docs # Swagger UI in browser |
| ``` |
|
|
| --- |
|
|
| ## 4. Environment Variable Configuration |
|
|
| All settings use the `FI_` prefix and are loaded by |
| `pydantic-settings` from (in priority order): |
|
|
| 1. Real environment variables. |
| 2. `.env` file at the project root. |
| 3. Field defaults in `config/settings.py`. |
|
|
| ### Quick reference |
|
|
| See [`docs/CONFIGURATION.md` §2](CONFIGURATION.md#2-quick-reference-all-settings) |
| for the full table of every setting, its env var name, type, and |
| default. |
|
|
| ### Common patterns |
|
|
| **Single-process dev:** |
|
|
| ```bash |
| export FI_DEBUG=true |
| export FI_LOG_LEVEL=DEBUG |
| python app.py |
| ``` |
|
|
| **Multi-process prod (systemd unit):** |
|
|
| ```ini |
| # /etc/systemd/system/face-intel.service |
| [Unit] |
| Description=Face Intel |
| After=network.target |
| |
| [Service] |
| Type=exec |
| User=face-intel |
| WorkingDirectory=/opt/face-intel |
| EnvironmentFile=/etc/face-intel/env |
| ExecStart=/opt/face-intel/.venv/bin/uvicorn app:app \ |
| --host 0.0.0.0 --port 8000 --workers 4 --no-access-log |
| Restart=on-failure |
| RestartSec=5s |
| |
| [Install] |
| WantedBy=multi-user.target |
| ``` |
|
|
| `/etc/face-intel/env`: |
|
|
| ```env |
| FI_ENVIRONMENT=production |
| FI_DEBUG=false |
| FI_LOG_LEVEL=INFO |
| FI_LOG_JSON=true |
| FI_DB_PATH=/var/lib/face-intel/face_intel.db |
| FI_AUDIT_LOG_PATH=/var/log/face-intel/audit.log |
| # ... etc |
| ``` |
|
|
| ### Loading env vars from a secrets manager |
|
|
| For Kubernetes, AWS, or HashiCorp Vault, write the secrets to env |
| vars in the container entrypoint before running uvicorn. The app |
| reads them at startup — once running, it doesn't poll for changes. |
|
|
| --- |
|
|
| ## 5. Database Setup |
|
|
| ### Engine |
|
|
| SQLite via the stdlib `sqlite3` module. No external database server |
| required. See [`storage/database.py`](../storage/database.py). |
|
|
| ### Schema |
|
|
| Auto-created on first run: |
|
|
| ```sql |
| CREATE TABLE jobs ( |
| id TEXT PRIMARY KEY, |
| kind TEXT NOT NULL, |
| status TEXT NOT NULL, |
| created_at TEXT NOT NULL, |
| started_at TEXT, |
| completed_at TEXT, |
| request TEXT NOT NULL, -- JSON blob |
| image_hash TEXT, |
| error TEXT |
| ); |
| |
| CREATE TABLE job_results ( |
| job_id TEXT PRIMARY KEY, |
| status TEXT NOT NULL, |
| report TEXT, -- JSON blob (UnifiedFaceReport) |
| error TEXT, |
| elapsed_ms REAL, |
| created_at TEXT NOT NULL, |
| FOREIGN KEY (job_id) REFERENCES jobs(id) |
| ); |
| |
| CREATE INDEX idx_jobs_status ON jobs(status); |
| CREATE INDEX idx_jobs_created ON jobs(created_at); |
| ``` |
|
|
| ### Path configuration |
|
|
| - Default: `data/face_intel.db` (relative to project root). |
| - Override via `FI_DB_PATH=/absolute/path/to/face_intel.db`. |
| - For tests: `FI_DB_PATH=:memory:` (in-memory, no disk file). |
|
|
| ### Thread safety |
|
|
| `Database` uses `sqlite3.connect(check_same_thread=False)` plus an |
| internal `threading.Lock` around every operation. Safe to share |
| across threads in one process. **Not** safe across multiple worker |
| processes — each worker gets its own connection (and its own |
| in-memory state). |
|
|
| ### Concurrency under multiple workers |
|
|
| If you run uvicorn with `--workers 4`, you'll have 4 independent |
| SQLite connections to the same file. SQLite handles this via file |
| locking — but write throughput drops sharply under contention. For |
| high-throughput multi-worker deployments: |
|
|
| 1. Use a separate PostgreSQL/MySQL backend (would require a new |
| `Database` implementation — not currently supported), OR |
| 2. Run a single worker and rely on async concurrency |
| (`orchestrator_max_concurrency`), OR |
| 3. Shard jobs across multiple Face Intel instances, each with its |
| own SQLite DB. |
|
|
| ### Job retention |
|
|
| Set `FI_JOB_RETENTION_DAYS=7` (default) to keep jobs for one week. |
| Run `Database.cleanup_old_jobs(retention_days)` from a cron job: |
|
|
| ```python |
| # scripts/cleanup_jobs.py |
| from config.settings import settings |
| from storage.database import Database |
| |
| db = Database(path=settings.db_path) |
| deleted = db.cleanup_old_jobs(settings.job_retention_days) |
| print(f"Deleted {deleted} old jobs") |
| ``` |
|
|
| Cron entry: |
|
|
| ```cron |
| 0 3 * * * /opt/face-intel/.venv/bin/python /opt/face-intel/scripts/cleanup_jobs.py |
| ``` |
|
|
| ### Backup |
|
|
| SQLite files can be backed up live using the `sqlite3` CLI: |
|
|
| ```bash |
| sqlite3 /var/lib/face-intel/face_intel.db ".backup /backup/face_intel-$(date +%F).db" |
| ``` |
|
|
| Or use the Online Backup API via Python: |
|
|
| ```python |
| import sqlite3 |
| src = sqlite3.connect("/var/lib/face-intel/face_intel.db") |
| dst = sqlite3.connect("/backup/face_intel.db") |
| src.backup(dst) |
| ``` |
|
|
| --- |
|
|
| ## 6. Storage Directory Layout |
|
|
| `config/settings.py` auto-creates these directories at import time: |
|
|
| ``` |
| data/ |
| ├── face_intel.db # SQLite database (jobs + results) |
| ├── audit.log # JSONL audit log |
| ├── models/ # Auto-downloaded model files |
| │ ├── deploy.prototxt # DNN Caffe SSD prototxt (~28 KB) |
| │ └── res10_300x300_ssd_iter_140000.caffemodel # (~10.7 MB) |
| ├── gallery/ # Known-faces reference store |
| │ ├── manifest.json # {person_name: [embedding_filenames]} |
| │ ├── alice_0.npy # Per-person face embeddings |
| │ └── bob_0.npy |
| ├── uploads/ # Source images saved by ArtifactStore |
| └── generated/ # Annotated images, montages |
| ``` |
|
|
| ### Volume mounting in containers |
|
|
| For Docker/Kubernetes, mount `data/` as a persistent volume: |
|
|
| ```yaml |
| volumes: |
| - face-intel-data:/app/data |
| ``` |
|
|
| For multi-replica deployments, `data/gallery/` must be on a **shared |
| filesystem** (NFS, EFS, S3-FUSE) so all replicas see the same known |
| faces. Otherwise each replica has its own gallery. |
|
|
| ### Model files |
|
|
| The DNN provider auto-downloads its model files on first use via |
| `urllib.request.urlretrieve` to `data/models/`. If your deployment |
| has no internet access: |
|
|
| 1. Pre-download the files on a build machine: |
|
|
| ```bash |
| mkdir -p data/models |
| curl -L -o data/models/deploy.prototxt \ |
| https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/deploy.prototxt |
| curl -L -o data/models/res10_300x300_ssd_iter_140000.caffemodel \ |
| https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel |
| ``` |
|
|
| 2. Bake them into your Docker image or volume. |
|
|
| ### Audit log |
|
|
| `utils.audit.audit_log()` appends one JSON line per sensitive |
| operation (gallery mutations, reverse image searches). Rotate with |
| `logrotate`: |
|
|
| ``` |
| # /etc/logrotate.d/face-intel |
| /var/log/face-intel/audit.log { |
| daily |
| rotate 30 |
| compress |
| missingok |
| notifempty |
| copytruncate |
| } |
| ``` |
|
|
| --- |
|
|
| ## 7. Docker Deployment |
|
|
| > **No Dockerfile is currently in the repo.** The snippet below is |
| > the recommended baseline. Track follow-up work in |
| > [§13](#13-follow-ups). |
|
|
| ### `Dockerfile` (recommended) |
|
|
| ```dockerfile |
| FROM python:3.11-slim AS base |
| |
| # OpenCV system deps + Chrome (for Selenium) + build tools for dlib |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| libgl1 libglib2.0-0 \ |
| build-essential cmake \ |
| chromium \ |
| && rm -rf /var/lib/apt/lists/* |
| |
| WORKDIR /app |
| |
| # Install Python deps first (better layer caching) |
| COPY requirements.txt . |
| RUN pip install --no-cache-dir -r requirements.txt |
| |
| # Copy app code |
| COPY . . |
| |
| # Create runtime dirs |
| RUN mkdir -p /app/data/models /app/data/gallery \ |
| /app/data/uploads /app/data/generated |
| |
| ENV FI_ENVIRONMENT=production \ |
| FI_DEBUG=false \ |
| FI_LOG_JSON=true \ |
| FI_DB_PATH=/app/data/face_intel.db \ |
| FI_AUDIT_LOG_PATH=/app/data/audit.log \ |
| FI_SELENIUM_HEADLESS=true |
| |
| EXPOSE 8000 |
| |
| # Single worker — multi-worker requires shared storage / external DB |
| CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", \ |
| "--no-access-log", "--log-level", "info"] |
| ``` |
|
|
| ### Build & run |
|
|
| ```bash |
| docker build -t face-intel:latest . |
| docker run -d --name face-intel \ |
| -p 8000:8000 \ |
| -v face-intel-data:/app/data \ |
| --env-file .env \ |
| face-intel:latest |
| ``` |
|
|
| ### `.dockerignore` |
|
|
| ``` |
| .venv/ |
| __pycache__/ |
| *.pyc |
| .pytest_cache/ |
| data/ |
| .env |
| *.log |
| .git/ |
| ``` |
|
|
| ### Multi-stage build for smaller image |
|
|
| If image size matters, split into a `builder` stage that compiles |
| `dlib` and a slim `runtime` stage: |
|
|
| ```dockerfile |
| FROM python:3.11-slim AS builder |
| RUN apt-get update && apt-get install -y build-essential cmake libgl1 libglib2.0-0 |
| WORKDIR /app |
| COPY requirements.txt . |
| RUN pip install --user -r requirements.txt |
| |
| FROM python:3.11-slim AS runtime |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| libgl1 libglib2.0-0 chromium && rm -rf /var/lib/apt/lists/* |
| WORKDIR /app |
| COPY --from=builder /root/.local /root/.local |
| COPY . . |
| ENV PATH=/root/.local/bin:$PATH |
| CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] |
| ``` |
|
|
| Final image: ~600 MB (vs ~1.5 GB without multi-stage). |
|
|
| ### Image variants |
|
|
| | Variant | Tag pattern | Use case | |
| |---|---|---| |
| | CPU-only | `face-intel:latest` | Default. Works everywhere. | |
| | CPU-only, no Selenium | `face-intel:cpu-slim` | Smaller (~400 MB). No Chrome. | |
| | GPU | `face-intel:gpu-cuda12` | Built on `nvidia/cuda:12.x-runtime`. For InsightFace/DeepFace acceleration. | |
|
|
| --- |
|
|
| ## 8. Kubernetes Deployment |
|
|
| ### Deployment + Service |
|
|
| ```yaml |
| apiVersion: apps/v1 |
| kind: Deployment |
| metadata: |
| name: face-intel |
| labels: |
| app: face-intel |
| spec: |
| replicas: 2 |
| selector: |
| matchLabels: |
| app: face-intel |
| template: |
| metadata: |
| labels: |
| app: face-intel |
| spec: |
| containers: |
| - name: face-intel |
| image: face-intel:latest |
| ports: |
| - containerPort: 8000 |
| envFrom: |
| - configMapRef: |
| name: face-intel-config |
| - secretRef: |
| name: face-intel-secrets |
| readinessProbe: |
| httpGet: |
| path: /health/ready |
| port: 8000 |
| initialDelaySeconds: 5 |
| periodSeconds: 10 |
| livenessProbe: |
| httpGet: |
| path: /health/live |
| port: 8000 |
| initialDelaySeconds: 30 |
| periodSeconds: 30 |
| resources: |
| requests: |
| cpu: "500m" |
| memory: "1Gi" |
| limits: |
| cpu: "2000m" |
| memory: "4Gi" |
| volumeMounts: |
| - name: data |
| mountPath: /app/data |
| volumes: |
| - name: data |
| persistentVolumeClaim: |
| claimName: face-intel-pvc |
| --- |
| apiVersion: v1 |
| kind: Service |
| metadata: |
| name: face-intel |
| spec: |
| selector: |
| app: face-intel |
| ports: |
| - port: 80 |
| targetPort: 8000 |
| --- |
| apiVersion: v1 |
| kind: PersistentVolumeClaim |
| metadata: |
| name: face-intel-pvc |
| spec: |
| accessModes: ["ReadWriteOnce"] |
| resources: |
| requests: |
| storage: 10Gi |
| ``` |
|
|
| ### ConfigMap + Secret |
|
|
| ```yaml |
| apiVersion: v1 |
| kind: ConfigMap |
| metadata: |
| name: face-intel-config |
| data: |
| FI_ENVIRONMENT: "production" |
| FI_DEBUG: "false" |
| FI_LOG_LEVEL: "INFO" |
| FI_LOG_JSON: "true" |
| FI_RATE_LIMIT_PER_MINUTE: "120" |
| FI_CACHE_ENABLED: "true" |
| FI_CACHE_TTL_SECONDS: "86400" |
| FI_DB_PATH: "/app/data/face_intel.db" |
| FI_AUDIT_LOG_PATH: "/app/data/audit.log" |
| FI_JOB_RETENTION_DAYS: "30" |
| # Provider enable flags... |
| FI_ENABLE_HAAR: "true" |
| FI_ENABLE_DNN: "true" |
| # ... etc |
| --- |
| apiVersion: v1 |
| kind: Secret |
| metadata: |
| name: face-intel-secrets |
| type: Opaque |
| stringData: |
| FI_SERPAPI_KEY: "..." |
| FI_BING_API_KEY: "..." |
| FI_TINEYE_PUBLIC_KEY: "..." |
| FI_TINEYE_PRIVATE_KEY: "..." |
| ``` |
|
|
| ### Multi-replica gallery sharing |
|
|
| If you run >1 replica, `data/gallery/` must be on a shared |
| filesystem. Options: |
|
|
| 1. **EFS / NFS** — mount an EFS access point at `/app/data/gallery`. |
| 2. **S3 + s3fs-fuse** — mount an S3 bucket. |
| 3. **Move the gallery to an external store** (Postgres, Redis) — |
| requires extending `ReferenceStore`. |
|
|
| ### Horizontal Pod Autoscaler |
|
|
| ```yaml |
| apiVersion: autoscaling/v2 |
| kind: HorizontalPodAutoscaler |
| metadata: |
| name: face-intel-hpa |
| spec: |
| scaleTargetRef: |
| apiVersion: apps/v1 |
| kind: Deployment |
| name: face-intel |
| minReplicas: 2 |
| maxReplicas: 10 |
| metrics: |
| - type: Resource |
| resource: |
| name: cpu |
| target: |
| type: Utilization |
| averageUtilization: 70 |
| ``` |
|
|
| --- |
|
|
| ## 9. Reverse Proxy Considerations |
|
|
| ### Nginx |
|
|
| ```nginx |
| upstream face_intel { |
| server 127.0.0.1:8000; |
| # For multi-worker: |
| # server 127.0.0.1:8001; |
| # server 127.0.0.1:8002; |
| } |
| |
| server { |
| listen 443 ssl http2; |
| server_name face-intel.example.com; |
| |
| ssl_certificate /etc/ssl/face-intel.crt; |
| ssl_certificate_key /etc/ssl/face-intel.key; |
| |
| client_max_body_size 25M; # match FI_MAX_REQUEST_BODY_BYTES |
| |
| location / { |
| proxy_pass http://face_intel; |
| proxy_set_header Host $host; |
| proxy_set_header X-Real-IP $remote_addr; |
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| proxy_set_header X-Forwarded-Proto $scheme; |
| |
| # Pass through request-id for tracing |
| proxy_set_header X-Request-ID $request_id; |
| |
| # Long-running jobs (full_pipeline can take minutes) |
| proxy_read_timeout 600s; |
| proxy_send_timeout 600s; |
| } |
| |
| # Health checks — bypass rate limiter implicitly (already bypassed in code) |
| location /health { |
| proxy_pass http://face_intel; |
| access_log off; |
| } |
| } |
| |
| server { |
| listen 80; |
| server_name face-intel.example.com; |
| return 301 https://$server_name$request_uri; |
| } |
| ``` |
|
|
| ### Caddy |
|
|
| ```caddyfile |
| face-intel.example.com { |
| reverse_proxy 127.0.0.1:8000 { |
| header_up X-Request-ID {http.request.uuid} |
| } |
| request_body { |
| max_size 25MB |
| } |
| } |
| ``` |
|
|
| ### Authentication at the proxy |
|
|
| Put authentication in front of the proxy — Face Intel has none by |
| default. Common patterns: |
|
|
| - **OAuth2 Proxy** — Google/GitHub login, hands a session cookie. |
| - **Cloudflare Access** — Zero-trust identity provider. |
| - **AWS API Gateway** with a Lambda authorizer. |
| - **mTLS** at the Nginx layer (`ssl_client_certificate`). |
|
|
| ### Cache coordination |
|
|
| Each uvicorn worker process has its own in-memory `Cache`. There is |
| **no shared cache** across workers. This means: |
|
|
| - Cache hit ratio is lower with more workers (each caches |
| independently). |
| - For high-throughput deployments, replace |
| [`storage/cache.py`](../storage/cache.py) with a Redis-backed |
| implementation that all workers share. |
|
|
| The `Cache` class has a clean interface (`get`, `set`, `invalidate`, |
| `clear`, `stats`) — a drop-in Redis replacement is straightforward. |
|
|
| ### WebSockets / Server-Sent Events |
|
|
| Currently Face Intel uses plain HTTP. If you add streaming endpoints |
| (SSE for job progress, WebSocket for live updates), make sure your |
| reverse proxy supports them — Nginx does by default, but some load |
| balancers (ALB) need explicit configuration. |
|
|
| --- |
|
|
| ## 10. Health Check Endpoints for Load Balancers |
|
|
| | Endpoint | Purpose | Recommended for | |
| |---|---|---| |
| | `GET /health` | Liveness — always returns `200 {"status":"ok"}`. | AWS ALB ping path, HAProxy `option httpchk`. | |
| | `GET /health/live` | Same as `/health`, separate path. | Kubernetes `livenessProbe`. | |
| | `GET /health/ready` | Readiness — currently always ready. | Kubernetes `readinessProbe`. | |
| | `GET /health/providers` | Per-provider health + circuit state. | Dashboards, alerting. | |
|
|
| ### AWS ALB |
|
|
| - **Health check path:** `/health` |
| - **Healthy threshold:** 2 |
| - **Unhealthy threshold:** 3 |
| - **Timeout:** 5 s |
| - **Interval:** 10 s |
|
|
| ### HAProxy |
|
|
| ``` |
| backend face_intel |
| option httpchk GET /health |
| http-check expect status 200 |
| server app1 127.0.0.1:8000 check |
| server app2 127.0.0.1:8001 check |
| ``` |
|
|
| ### Kubernetes probes |
|
|
| ```yaml |
| readinessProbe: |
| httpGet: |
| path: /health/ready |
| port: 8000 |
| initialDelaySeconds: 5 |
| periodSeconds: 10 |
| failureThreshold: 3 |
| livenessProbe: |
| httpGet: |
| path: /health/live |
| port: 8000 |
| initialDelaySeconds: 30 # let models load |
| periodSeconds: 30 |
| failureThreshold: 3 |
| ``` |
|
|
| ### Alerting on circuit state |
|
|
| Poll `/health/providers` and alert when any provider's |
| `circuit_open == true` for more than 5 minutes: |
|
|
| ```bash |
| curl -s http://localhost:8000/health/providers | \ |
| jq -e '.providers[] | select(.circuit_open == true)' && \ |
| trigger_alert "Face Intel circuit breaker open" |
| ``` |
|
|
| --- |
|
|
| ## 11. TLS / HTTPS |
|
|
| ### Option A — terminate at the reverse proxy (recommended) |
|
|
| Run Face Intel on plain HTTP behind Nginx/Caddy/ALB that terminates |
| TLS. The app doesn't know about TLS. |
|
|
| ### Option B — terminate at uvicorn |
|
|
| ```bash |
| uvicorn app:app --host 0.0.0.0 --port 8443 \ |
| --ssl-keyfile /etc/ssl/face-intel.key \ |
| --ssl-certfile /etc/ssl/face-intel.crt |
| ``` |
|
|
| Useful for direct exposure without a proxy (e.g. internal |
| microservice mesh). |
|
|
| ### HSTS |
|
|
| If you terminate at the proxy, add HSTS: |
|
|
| ```nginx |
| add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; |
| ``` |
|
|
| --- |
|
|
| ## 12. Operational Runbook |
|
|
| ### Starting up |
|
|
| ```bash |
| # 1. Verify env file is present |
| test -f .env || (echo "Missing .env"; exit 1) |
| |
| # 2. Verify deps are installed |
| python -c "import fastapi, cv2, pydantic_settings; print('OK')" |
| |
| # 3. Verify DB path is writable |
| python -c "import os; os.makedirs('$(dirname $(grep FI_DB_PATH .env | cut -d= -f2))', exist_ok=True); print('OK')" |
| |
| # 4. Start |
| uvicorn app:app --host 0.0.0.0 --port 8000 |
| ``` |
|
|
| ### Shutting down |
|
|
| Send `SIGTERM` (Ctrl-C or `kill <pid>`). The lifespan handler closes |
| the SQLite connection cleanly: |
|
|
| ```python |
| # api/main.py |
| async def lifespan(app): |
| ... |
| yield |
| container.database.close() |
| ``` |
|
|
| ### Upgrading |
|
|
| 1. Pull new code. |
| 2. `pip install -r requirements.txt` (in case new deps were added). |
| 3. Run tests: `python -m pytest tests/ -v`. |
| 4. Restart the server. |
|
|
| SQLite schema migrations are forward-compatible (new columns / |
| tables use `CREATE TABLE IF NOT EXISTS`). No explicit migration tool |
| is currently bundled. |
|
|
| ### Rotating API keys |
|
|
| 1. Update `FI_SERPAPI_KEY` (etc.) in your secrets manager / `.env`. |
| 2. Restart the server. |
| 3. Verify with `curl /providers/serpapi | jq .available`. |
|
|
| ### Clearing the cache |
|
|
| ```bash |
| # API |
| curl -X DELETE http://localhost:8000/cache |
| |
| # Or direct on the host (no-op for in-memory cache, but useful |
| # if you've moved to a Redis-backed implementation) |
| ``` |
|
|
| ### Cleaning old jobs |
|
|
| ```bash |
| python scripts/cleanup_jobs.py |
| # Or run via cron — see §5 |
| ``` |
|
|
| ### Inspecting the gallery |
|
|
| ```bash |
| curl http://localhost:8000/faces/gallery | jq . |
| ``` |
|
|
| ### Viewing audit log |
|
|
| ```bash |
| tail -f /var/log/face-intel/audit.log | jq . |
| ``` |
|
|
| --- |
|
|
| ## 13. Follow-ups |
|
|
| The architecture is production-grade but the following ops |
| infrastructure is not yet in the repo: |
|
|
| | Item | Priority | Notes | |
| |---|---|---| |
| | `Dockerfile` | high | See [§7](#7-docker-deployment) for the recommended baseline. | |
| | `docker-compose.yml` | medium | For local multi-service dev (Face Intel + Redis + mock reverse-search). | |
| | Redis-backed `Cache` | medium | Drop-in replacement for in-memory `Cache`; needed for >1 worker. | |
| | Prometheus metrics endpoint | medium | Expose `/metrics` with `prometheus_client`. Currently only JSON `/stats`. | |
| | Database migration tool | low | Alembic — currently schemas use `CREATE TABLE IF NOT EXISTS`. | |
| | Reference gallery REST API | medium | `POST /faces/gallery/{name}` to add embeddings. Currently only `add_known_person()` on the service. | |
| | Background job cleanup | medium | `asyncio.create_task` in lifespan to run `cleanup_old_jobs` daily. | |
| | Multi-process gallery sharing | high | Move `ReferenceStore` to Postgres/Redis for >1 replica. | |
|
|
| --- |
|
|
| ## See Also |
|
|
| - [`docs/CONFIGURATION.md`](CONFIGURATION.md) — every `FI_*` env var. |
| - [`docs/API_REFERENCE.md`](API_REFERENCE.md) — health endpoints. |
| - [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md) — startup failures, |
| model download issues, dlib compilation. |
| - [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) — composition root, |
| lifespan, DI container. |
|
|