Skip to content

Self-hosting

Container

The repository's Dockerfile builds one image with the web app, the API, the docs site and the CLI:

docker build -t corpusos .
docker run --rm -p 8080:8080 -v "$PWD/data:/data" \
  -e CORPUSOS_WEB_AUTH=firebase -e CORPUSOS_ADMINS=me@example.com \
  -e CORPUSOS_FIREBASE_PROJECT_ID=my-project -e CORPUSOS_FIREBASE_API_KEY=... \
  -e CORPUSOS_FIREBASE_AUTH_DOMAIN=my-project.firebaseapp.com \
  -e GOOGLE_APPLICATION_CREDENTIALS=/data/sa.json corpusos

With no arguments, the image creates the corpus at /data/corpus on first start (owned by CORPUS_OWNERS, default CORPUSOS_ADMINS) and serves the web app on 0.0.0.0:$PORT. Any other arguments run as a corpusos command, for example docker run … corpusos status -C /data/corpus. Off loopback the header mode is refused, so choose firebase or jwt (Web app and API). For a quick local trial without sign-in, use corpusos web outside a container.

Variable Meaning
CORPUSOS_WEB_AUTH firebase, jwt or header
CORPUSOS_PUBLIC_URL the https URL users open
CORPUSOS_ADMINS comma-separated admin emails
CORPUSOS_ALLOWED_EMAILS, CORPUSOS_ALLOWED_DOMAINS who may sign in
CORPUSOS_FIREBASE_PROJECT_ID, CORPUSOS_FIREBASE_API_KEY, CORPUSOS_FIREBASE_AUTH_DOMAIN the Firebase web config (public values)
CORPUSOS_JWT_* HEADER, ISSUER, AUDIENCE, JWKS_URL for proxy sign-in
CORPUSOS_STORAGE_DIR the storage folder (default <corpus>/storage)
CORPUSOS_CATALOG_PATH a local-disk catalog when the corpus is on network storage
CORPUSOS_SYNC_INTERVAL_MINUTES sync storage on a timer (0 = off)
CORPUSOS_JOBS background (default) or inline: run ingest inside the request, for hosts that only give CPU during requests
CORPUS_OWNERS the default access list for a new corpus (--owner)

Secrets never go into config files. Put them in environment variables or your secret manager, and reference them as ${VAR}.

Google Cloud Run (example)

Two shapes:

Free tier (scale to zero) Always on
Instances --min-instances 0 --max-instances 1 --min-instances 1 --max-instances 1 --no-cpu-throttling
Ingest CORPUSOS_JOBS=inline: the upload or sync request waits for it background jobs, plus the CORPUSOS_SYNC_INTERVAL_MINUTES timer
Files copied into the bucket picked up when someone presses Sync (or calls POST /api/sync) also on the timer
Cost within Cloud Run's free tier for light use; the first request after idle starts the app (a few seconds) one instance billed around the clock

Either way the catalog is snapshotted after every write and on shutdown.

  • Storage: a Cloud Storage bucket mounted as a volume at /data. The corpus goes in /data/corpus, storage in /data/corpus/storage, and CORPUSOS_CATALOG_PATH=/tmp/catalog/catalog.db.
  • Instances: exactly one (--max-instances 1 --min-instances 1) with --no-cpu-throttling, so ingest jobs and the sync timer keep running between requests.
  • Sign-in: firebase (the service runs public, and the app enforces sign-in), or IAP with jwt.
  • Files placed in the bucket directly (gcloud storage cp report.pdf gs://BUCKET/corpus/storage/finance/) are picked up by the next sync.
gcloud run deploy corpusos --image REGION-docker.pkg.dev/PROJECT/REPO/corpusos:TAG \
  --region us-central1 --max-instances 1 --min-instances 1 --no-cpu-throttling --memory 2Gi \
  --execution-environment gen2 --add-volume name=data,type=cloud-storage,bucket=BUCKET \
  --add-volume-mount volume=data,mount-path=/data \
  --set-env-vars CORPUSOS_WEB_AUTH=firebase,CORPUSOS_CATALOG_PATH=/tmp/catalog/catalog.db,... \
  --allow-unauthenticated

Backups

The corpus tree is the backup. It holds the raw bytes, every revision, and a catalog snapshot after each write. Turn on object versioning on the bucket, or snapshot the volume.