Skip to content

Deployment

Docker Compose (Development)

docker compose up --build

Starts the local stack: bootstrap, API, Admin, PostgreSQL, and MinIO.

The checked-in compose file is for local development. It does not target production domains by default. Its default CORS setting allows only local admin origins, and the admin container waits for a healthy API container before starting.

The bootstrap service is a one-shot setup job that prepares schema and storage prerequisites before the long-running services start.

Services

Service Build Image Ports
bootstrap ./Dockerfile python:3.14-slim + uv
api ./Dockerfile python:3.14-slim + uv 8000
admin ./admin/Dockerfile node:24-slim → node:24-alpine 4321
db postgres:18-alpine 5432
minio minio/minio:RELEASE.2025-04-22T22-12-26Z 9000, 9001

Build Args

  • FACE_API_MODEL: Model to download at build time (default: scrfd_10g)
  • PUBLIC_API_URL (admin build): defaults to http://localhost:8000 in the local compose stack so the admin UI never targets production by accident

Admin runtime env

  • ADMIN_SESSION_API_URL: defaults to the internal API service URL used by the Node session-gating server for auth checks

Volumes

  • pgdata: PostgreSQL data directory
  • minio-data: MinIO object storage

Production Configuration

API Service

environment:
  FACE_API_MODEL: scrfd_10g
  FACE_API_STORAGE_ENABLED: "true"
  FACE_API_DATABASE_URL: "postgresql+asyncpg://user:pass@db-host:5432/faceapi"
  FACE_API_S3_ENDPOINT: "https://s3.amazonaws.com"
  FACE_API_S3_ACCESS_KEY: "${AWS_ACCESS_KEY_ID}"
  FACE_API_S3_SECRET_KEY: "${AWS_SECRET_ACCESS_KEY}"
  FACE_API_S3_BUCKET: "xylolabs-face-api-prod"
  FACE_API_S3_REGION: "us-east-1"
  FACE_API_S3_PUBLIC_ENDPOINT: "https://static.face-api.xylolabs.com"
  FACE_API_CORS_ORIGINS: "https://face-api-admin.xylolabs.com"
  FACE_API_ADMIN_SESSION_SECRET: "${FACE_API_ADMIN_SESSION_SECRET}"
  FACE_API_ADMIN_SESSION_TTL_SECONDS: "43200"
  FACE_API_ADMIN_LOGIN_RATE_LIMIT: "10/minute"
  FACE_API_ORT_THREADS: "2"

Admin Service

build:
  context: ./admin
  args:
    PUBLIC_API_URL: "https://face-api.xylolabs.com"
environment:
  ADMIN_SESSION_API_URL: "http://api:8000"
  • PUBLIC_API_URL should point to the browser-facing API origin.
  • ADMIN_SESSION_API_URL should point to the network path the Node session-gating server can actually reach from inside the container/runtime.

Concurrency Tuning

Variable Default Description
FACE_API_MAX_CONCURRENT_INFERENCE 2 Max parallel ONNX inference calls per worker
FACE_API_MAX_CONCURRENT_REQUESTS 10 Max in-flight requests before 503
FACE_API_ORT_THREADS 0 (auto) ONNX Runtime threads per inference call

Scaling

  • Workers: --workers N in uvicorn CMD (default: 1). Each worker loads the model separately (~200MB).
  • ORT Threads: Set to core count (e.g., FACE_API_ORT_THREADS=2 for 2 cores).
  • Inference Concurrency: Keep MAX_CONCURRENT_INFERENCE <= core count to avoid CPU thrashing.
  • DB Pool: SQLAlchemy pool_size=10, max_overflow=20 (configured in database.py).

Health Check

curl http://localhost:8000/health
using var client = new HttpClient();
var response = await client.GetAsync("http://localhost:8000/health");
Console.WriteLine(await response.Content.ReadAsStringAsync());

Docker HEALTHCHECK runs every 30s with 10s startup grace period.

Domain Routing

Domain Target
face-api.xylolabs.com API service :8000
face-api-admin.xylolabs.com Admin UI :4321
docs.face-api.xylolabs.com MkDocs static site
static.face-api.xylolabs.com Public processed-asset host backed by MinIO when configured

Use a reverse proxy or load balancer that matches your own environment. The repository does not provision nginx, TLS certificates, DNS, or firewall rules for you.

Production (Oracle Cloud)

This section is an operator note for the currently documented manual environment. It does not describe infrastructure that the repository can create or verify on its own.

The API is deployed at 130.162.132.159 (Oracle Cloud, Ampere Altra ARM, 2 cores / 4GB RAM).

Infrastructure

Component Details
VM OCI Ampere A1 (aarch64), Ubuntu 24.04
Docker Docker Engine 29.x + Compose v5.x
Reverse Proxy nginx 1.24 with HTTP→HTTPS redirect
SSL Let's Encrypt via certbot (auto-renews)
Domains face-api.xylolabs.com, face-api-admin.xylolabs.com, docs.face-api.xylolabs.com, static.face-api.xylolabs.com

Tuning for 2-core/4GB

# docker-compose.yml overrides
FACE_API_MAX_CONCURRENT_INFERENCE: "1"
FACE_API_MAX_CONCURRENT_REQUESTS: "8"
FACE_API_ORT_THREADS: "2"
# Dockerfile: --workers 1 (avoid double model loading)
# Memory limit: 768MB for API container

SSH Access

ssh -i ~/.ssh/xylolabs-prod.pem ubuntu@130.162.132.159

Redeploy

# From local machine
rsync -avz --exclude='.venv' --exclude='.git' --exclude='models/*.onnx' \
  --exclude='node_modules' --exclude='__pycache__' \
  -e "ssh -i ~/.ssh/xylolabs-prod.pem" \
  ./ ubuntu@130.162.132.159:~/face-api/

# On remote
cd ~/face-api && docker compose up -d --build

Nginx Config

If you use nginx in an environment like the current Oracle Cloud deployment, the active config lives outside the repo (for example /etc/nginx/sites-available/face-api.conf). Certbot-managed SSL directives are also external runtime state, not versioned here.

Firewall

Ports 22, 80, and 443 must be opened in whatever host firewall and cloud-network controls your environment uses. In the current Oracle Cloud deployment that means both host-level rules and OCI Security List ingress.

Environment Variables

Full list in API Documentation (English).

Migration Path

The normal serving app no longer mutates schema on every startup. Local/dev bootstrap is handled explicitly by scripts/bootstrap_runtime.py (and by the one-shot bootstrap service in docker-compose.yml).

For production migrations:

  1. Add Alembic: uv add alembic
  2. Initialize: alembic init migrations
  3. Configure migrations/env.py to use async engine
  4. Generate migrations: alembic revision --autogenerate -m "description"
  5. Apply: alembic upgrade head

The PostgreSQL service in docker-compose is designed to be migrated to a separate database server — just update FACE_API_DATABASE_URL.