Files
Primora/DEPLOYMENT_GUIDE.md
Tomáš DvořákandDevin d44bbcceda
CI / Backend Tests (push) Canceled after 0s
CI / Frontend Tests (push) Canceled after 0s
CI / Build All Services (push) Canceled after 0s
CI / Docker Build (auth) (push) Canceled after 0s
CI / Docker Build (backend) (push) Canceled after 0s
CI / Docker Build (frontend) (push) Canceled after 0s
CI / Publish edge images (auth) (push) Canceled after 0s
CI / Publish edge images (backend) (push) Canceled after 0s
CI / Publish edge images (frontend) (push) Canceled after 0s
CI / Migration Up/Down (push) Canceled after 0s
CI / Compose E2E Smoke (push) Canceled after 0s
CI / Lint (push) Canceled after 0s
fix: QA hardening across desktop connect, CLI, setup, and PWA update flow (#52)
- desktop: probe-then-persist server URL — scheme-less input tries https
  then http; a failed probe no longer saves a dead URL that would trap
  the next launch in a redirect to a broken address
- frontend: close the create-collection modal on successful submit
  (backdrop stayed mounted and intercepted navigation)
- frontend: reload once on service-worker controllerchange so returning
  users don't run a stale cached shell after a deploy
- frontend: gate dashboard empty states on resource loading to stop the
  "No project selected" / "Create your workspace" flash
- cli: resolve scheduled-job refs by name or id for jobs:run, jobs:runs,
  and jobs:rm, matching bucket/org resolution
- cli: correct stale pk_live_/pk_test_ help text to the real prm_ format;
  align README command spellings and demo seed prefixes
- setup: rewrite all public URL vars (auth, vite, backend) and cookie
  domain for the entered host — https for domains, http for IPs — with
  anchored full-line substitutions so stale ports/paths can't survive
- docs: VITE_* vars never reach the frontend image build; the app
  resolves API/auth URLs same-origin at runtime, so host changes need no
  rebuild — noted HTTPS requirement for PWA/secure-context features

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

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-03 18:52:53 +02:00

180 lines
8.3 KiB
Markdown

# 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
```bash
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/`):
```bash
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 — informational only; the shipped image resolves API/auth URLs same-origin at runtime, so no rebuild is needed when the host changes | 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):
```yaml
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`, `BACKEND_PUBLIC_URL`, and
`COOKIE_DOMAIN` to the https origin and restart the stack
(`docker compose up -d`). The frontend resolves API/auth URLs
same-origin, so no image rebuild is needed — HTTPS is still required for
PWA install, service workers, and other secure-context features on
non-localhost origins.
## 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:
```bash
./scripts/backup.sh # → backups/primora-<stamp>.tar.gz
```
Restore (destructive — stops writes):
```bash
docker compose stop backend auth
./scripts/restore.sh backups/primora-<stamp>.tar.gz
docker compose up -d
```
Cron example (nightly, keep 14):
```cron
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:
```bash
./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.