Skip to content

Self-hosting (Docker)

PLUR Enterprise runs in Docker Compose behind Caddy; works on public internet (auto-TLS) or intranet (your own cert). This page is the shape of the deployment — the full installation guide ships with the deployment bundle.

  • Linux server (Ubuntu 22.04+ recommended)
  • Docker Engine 24+ and Docker Compose v2
  • ≥ 2 vCPU, ≥ 4 GB RAM, ≥ 20 GB disk
  • Ports 80 and 443 reachable from your users
  • A hostname or IP (public domain or internal)
  • Container registry access (images are pulled from GHCR by default; a custom/air-gapped registry is supported via PLUR_REGISTRY)

The bundle contains:

docker-compose.prod.yml
Caddyfile.example
.env.example
docker/ # DB init scripts (extensions + role bootstrap)
Terminal window
cp .env.example .env
$EDITOR .env

Required values:

VariableExampleNotes
PLUR_BASE_URLhttps://plur.your-org.comPublic-facing URL
DATABASE_URLpostgresql://plur_app:...@db:5432/plur_enterpriseRuntime DSN — least-privilege plur_app role
MIGRATION_DATABASE_URLpostgresql://plur_admin:...@db:5432/plur_enterpriseAdmin DSN — used only by the startup migration step
JWT_SECRET<openssl rand -base64 32>Token signing
ORG_ID / ORG_NAMEmyorg / "My Organization"Org identifier + display name
ADMIN_USERSalice,bobIdP usernames granted the admin role
PLUR_OIDC_ENCRYPTION_KEY<openssl rand -base64 32>Encrypts SSO secrets + webhook signing keys at rest
AUDIT_HMAC_KEY<openssl rand -base64 32>Signs the tamper-evident audit chain
POSTGRES_ADMIN_PASSWORD / POSTGRES_APP_PASSWORDMust match the two DSNs above
PLUR_SMTP_*optionalIf unset, email runs in stub mode (logs to console)

There is no local admin account: admins are IdP-authenticated users listed in ADMIN_USERS (fine-grained roles via ROLE_USERS).

Optional auth:

VariableNotes
GITHUB_CLIENT_ID / _SECRET / GITHUB_ORGSGitHub OAuth
GITLAB_CLIENT_ID / _SECRET / GITLAB_URLGitLab OAuth (self-hosted GitLab works)
OIDC / SAMLConfigured from the /admin dashboard, not env vars
Terminal window
cp Caddyfile.example Caddyfile
$EDITOR Caddyfile

For public domains, the default config gets free TLS from Let’s Encrypt. For intranet, swap the auto-TLS for tls /path/to/cert.pem /path/to/key.pem.

Terminal window
docker compose -f docker-compose.prod.yml up -d

What this starts:

  • db — Postgres 16 with Apache AGE + pgvector (prebuilt image).
  • app — the Node server.
  • caddy — TLS termination + reverse proxy.

First start takes 1–2 minutes: extensions load, DB roles are created, and the app runs all migrations (via MIGRATION_DATABASE_URL; runtime traffic then uses least-privilege DATABASE_URL).

Terminal window
curl https://plur.your-org.com/health

Expected (JSON):

{
"status": "ok",
"version": "0.x.y",
"uptime_seconds": 12
}

/status serves a human-readable HTML status page.

Log in at /admin via your configured IdP; users listed in ADMIN_USERS get admin access.

Add at least one IdP for browser users (OIDC/SAML/GitHub OAuth/GitLab OAuth) from the admin dashboard. For agents (Claude Code, Cursor, OpenClaw), issue API keys from /me/api-keys — see Authentication for the right method per client.

Daily GPG-encrypted Postgres dumps, driven by the BACKUP_* env vars (BACKUP_ENCRYPTION_KEY, retention by days and count). Off-host push to a separate machine via rsync-over-SSH (or restic/object storage) is documented in the bundled setup guide. Store the encryption passphrase in a vault — without it, backups are unrecoverable.

Terminal window
cd /opt/plur-enterprise
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

Migrations are idempotent and run automatically on container start. Upgrades require zero manual steps by design.

Two roles, two DSNs: plur_admin (bootstrap + privileged migrations) and plur_app (least-privilege runtime). The compose stack creates both automatically. See Postgres & backups.

Structured JSON via pino. Tail:

Terminal window
docker compose -f docker-compose.prod.yml logs -f app

Audit events go to the append-only audit_log table; query via the admin dashboard or directly with SQL.