Files
Primora/DEPLOYMENT_GUIDE.md
T
Tomas DvorakandDevin 9ae1c22af0 feat: Sandboxed function execution via FUNCTIONS_DRIVER=docker
Each invoke runs in an ephemeral locked-down container — no network,
read-only rootfs, 128MiB/0.5CPU/64-pid caps, non-root, cap-drop ALL.
Source travels as base64 in the container env and materializes in the
tmpfs, so no bind mount is needed (Docker Desktop / remote daemons work).
Timeout force-removes the named container.

exec driver stays the default; the trust-model docs now describe both.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 20:45:33 +02:00

8.0 KiB

Primora Deployment Guide

Primora ships as a Docker Compose stack: nginx (edge), frontend (static SPA), auth (Better Auth / Hono), backend (Go API), postgres, and dragonfly (Redis-compatible cache). The base docker-compose.yml is the production surface — dev-only services like Mailpit live in docker-compose.dev.yml.

Prerequisites

  • Docker Engine 24+ with the Compose plugin
  • A host with 2 GB free RAM minimum (postgres + dragonfly + services)
  • A domain pointing at the host if you want TLS

Quick start

git clone <repo> && cd Primora
./scripts/setup.sh            # writes .env with generated secrets
docker compose up -d          # production surface only

For local development (includes Mailpit at /mailpit/):

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

setup.sh generates JWT_SECRET, BETTER_AUTH_SECRET, and PRIMORA_ENCRYPTION_KEY. Do not reuse the example values anywhere public.

Environment reference

Variable Purpose Required
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB database credentials yes
DATABASE_URL backend → postgres DSN yes
DRAGONFLY_URL backend → cache DSN yes
JWT_SECRET signs API JWTs (JWT_ISSUER, JWT_AUDIENCE, JWT_TTL_SECONDS tune tokens) yes
BETTER_AUTH_SECRET auth session/signing secret yes
PRIMORA_ENCRYPTION_KEY AES-256 key (64 hex chars) for project secrets (Vault page, secret:// job refs), integration credentials + webhook secrets at rest production
AUTH_BASE_URL / BETTER_AUTH_URL public auth URL (https://host/auth) yes
AUTH_INTERNAL_BASE_URL backend → auth internal URL (http://auth:3001) yes
VITE_APP_URL / VITE_AUTH_BASE_URL / VITE_API_BASE_URL frontend public URLs — baked into the frontend image at build time yes
BACKEND_STORAGE_ROOT object storage directory inside the backend container (local driver) yes
BACKEND_STORAGE_DRIVER local (default) or s3 — S3-compatible object storage no
S3_ENDPOINT / S3_REGION / S3_BUCKET S3 API endpoint (empty endpoint → AWS s3.<region>.amazonaws.com), region, bucket with s3
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY S3 credentials with s3
S3_PREFIX / S3_PATH_STYLE optional key prefix; path-style URLs (default true, needed by MinIO/Garage — set false for AWS virtual-hosted) no
S3_PUBLIC_ENDPOINT client-facing S3 endpoint used for presigned URLs when S3_ENDPOINT is internal-only (e.g. http://minio:9000); signatures bind the public host no
FUNCTIONS_DRIVER Function execution driver: exec (default — runtime binary on the backend host; code runs with backend privileges) or docker — each invoke runs in an ephemeral locked-down container (no network, read-only fs, 128 MiB, non-root). Docker mode needs /var/run/docker.sock mounted into the backend (see the commented volume in docker-compose.yml) and the runtime images pulled (oven/bun, denoland/deno). Source code is capped at 256 KiB under the docker driver no
FUNCTIONS_RUNTIME / FUNCTIONS_TIMEOUT_SECONDS / FUNCTIONS_MAX_OUTPUT_BYTES Functions: runtime binary (auto resolves bun/deno — exec driver only; docker driver uses the matching official image), per-run timeout (default 30 s), captured-output cap (default 64 KiB) no
AUTH_ADMIN_EMAILS comma-separated emails promoted to auth admin on boot no
USER_RATE_LIMIT_PER_MINUTE / API_KEY_RATE_LIMIT_PER_MINUTE per-identity API rate limits (defaults 240 / 600) no
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORD / SMTP_SECURE / MAIL_FROM transactional mail; without it verification/reset emails go nowhere production
RESEND_API_KEY alternative to SMTP — Resend API no
*_CLIENT_ID / *_CLIENT_SECRET GitHub / Google / Discord / Microsoft OAuth no
NGINX_PORT host port for the edge (default 80) no

TLS

Primora does not terminate TLS itself — put a reverse proxy in front of nginx's port 80. The nginx config sets X-Forwarded-*-aware headers; pass them through.

Caddy (Caddyfile):

primora.example.com {
    reverse_proxy localhost:80
}

Traefik (compose labels on the nginx service):

labels:
  - traefik.enable=true
  - traefik.http.routers.primora.rule=Host(`primora.example.com`)
  - traefik.http.routers.primora.entrypoints=websecure
  - traefik.http.routers.primora.tls.certresolver=letsencrypt

Once TLS is live, update VITE_APP_URL, VITE_AUTH_BASE_URL, AUTH_BASE_URL, BETTER_AUTH_URL, and COOKIE_DOMAIN to the https origin and rebuild the frontend image (docker compose build frontend).

SMTP / email

Auth sends verification and password-reset email through SMTP (SMTP_* + MAIL_FROM) or Resend (RESEND_API_KEY). In development the docker-compose.dev.yml overlay runs Mailpit; mail is captured at http://localhost/mailpit/ and nothing leaves the host. The production compose file contains no mail sink — configure real SMTP before inviting users.

Backups

scripts/backup.sh produces a tarball with a pg_dump -Fc of the database and a tar of the backend_storage volume:

./scripts/backup.sh            # → backups/primora-<stamp>.tar.gz

Restore (destructive — stops writes):

docker compose stop backend auth
./scripts/restore.sh backups/primora-<stamp>.tar.gz
docker compose up -d

Cron example (nightly, keep 14):

17 3 * * * cd /opt/primora && ./scripts/backup.sh && \
  ls -1t backups/primora-*.tar.gz | tail -n +15 | xargs -r rm

systemd timer example is in PRODUCTION_READINESS.md.

Upgrades

Migrations run automatically on backend boot (goose). Safe order:

./scripts/backup.sh                       # 1. snapshot first
docker compose pull && docker compose build   # 2. new images
docker compose up -d                      # 3. rolling recreate; backend migrates on boot
./scripts/smoke-e2e.sh                    # 4. verify health, auth, ingest

goose migrations include Down sections; goose -dir apps/backend/db/migrations postgres "$DATABASE_URL" down rolls back one version if a release misbehaves.

Storage constraint

Object storage defaults to the local filesystem (backend_storage volume) — durable, backed-up disk on a single host. For multi-node or external object stores set BACKEND_STORAGE_DRIVER=s3 + the S3_* variables above (AWS, MinIO, Garage, R2 all work).

Functions trust model

Two drivers, two trust levels:

  • exec (default) — function code runs as a child process of the backend: same host, same privileges, no sandbox. Treat every project member who can edit a function as holding a shell on the backend host. Appropriate for single-team self-hosted deployments.
  • docker — each invoke runs in an ephemeral container: no network, read-only rootfs, 128 MiB memory, half a CPU, 64 pids, non-root, all capabilities dropped. Requires the Docker socket mounted into the backend (commented volume in docker-compose.yml) — note the socket is itself root-equivalent on the host, so this isolates function code, not operators. Source code capped at 256 KiB. For hostile multi-tenant workloads, a dedicated microVM runtime (Firecracker/gVisor) remains the roadmap item.

Observability

  • GET /api/v1/metrics — Prometheus exposition (unauthenticated; restrict at the edge if the instance is public — see the commented rule in infra/nginx/default.conf).
  • GET /api/v1/health/liveness / readiness — compose healthchecks use the same endpoints.
  • Structured JSON logs on stdout for every service — ship with your normal docker log driver.

API key scopes

Keys are created with scopes: ingest, read, write, admin. admin unlocks everything; write unlocks read + ingest + non-admin mutations; read/ingest are single-purpose. Keys without stored scopes (pre-scope keys) behave as admin for backwards compatibility — rotate them into scoped keys at your leisure.