Supported deployment

ProBeya’s supported self-hosted path is the checked-in Docker Compose stack. Every production command must combine the base file and production overlay, in that order:

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  <command>

docker/docker-compose.prod.yml is an overlay, not a standalone Compose model. There is no separate api service: Next.js hosts the tRPC and HTTP routes in web.

The hardened MVP excludes cron handlers, GraphQL, REST v1, the broad MCP tool catalog, enterprise SSO/SAML, dedicated PPM/ingest/report/integration routes, public sharing links, and global template galleries until they have tenant-aware RLS contracts. Nginx returns 404 for the excluded API routes, and the production profile fixes Authentik and Azure AD flags to false.

The hardened product shell advertises only Dashboard, Forms, Settings, and the dynamic Workspace → Project → Board tree. Settings are limited to profile, security, organization, members, and billing. The broader enterprise navigation remains available only in non-hardened development builds.

Requirements

ResourceMinimumRecommended production baseline
CPU2 cores4+ cores
RAM4 GB8+ GB
Storage20 GB SSD100+ GB persistent SSD plus off-host backups
OSLinuxCurrent Ubuntu LTS or Debian stable
Docker24+ with Compose v2Current supported release
NetworkPublic IP/domainTLS on ports 80 and 443

The bundled database is PostgreSQL 16.

Production topology

ServiceInternal portResponsibility
nginx80 / 443Public TLS entry point and hostname routing
web8000Next.js UI, tRPC/HTTP API, outbox worker
ws8003WebSocket collaboration
docusaurus3001Generated documentation
postgres5432PostgreSQL 16
redis6379Cache and pub/sub
minio9000 / 9001Object API and console
clamav3310Attachment scanning
minio-initone shotPrivate bucket/application-user bootstrap
ownershipone shotExisting-schema ownership adoption
schemaone shotSchema, reviewed SQL, and RLS verification

Only Nginx is publicly exposed. The production overlay binds infrastructure ports to loopback for operator access.

Configure

1

Clone and prepare the environment

git clone https://github.com/obeya-cloud/obeya.git
cd obeya
cp .env.example .env

Replace every placeholder. The critical wiring is:

POSTGRES_DB=probeya
POSTGRES_USER=probeya
POSTGRES_PASSWORD=<random-superuser-password>
PROBEYA_APP_DB_PASSWORD=<random-runtime-password>
PROBEYA_ADMIN_DB_PASSWORD=<different-schema-owner-password>

DATABASE_URL_DOCKER=postgresql://probeya_app:<encoded-runtime-password>@postgres:5432/probeya
DATABASE_ADMIN_URL_DOCKER=postgresql://probeya_admin:<encoded-schema-owner-password>@postgres:5432/probeya

REDIS_PASSWORD=<random-redis-password>
REDIS_URL_DOCKER=redis://:<encoded-redis-password>@redis:6379

MINIO_ROOT_USER=<random-root-access-key>
MINIO_ROOT_PASSWORD=<random-root-secret-key>
S3_ACCESS_KEY=<different-application-access-key>
S3_SECRET_KEY=<different-application-secret-key>
S3_ENDPOINT=http://minio:9000
S3_PUBLIC_URL=https://files.example.com
S3_BUCKET=probeya-uploads

AUTH_SECRET=<at-least-32-random-bytes>
WS_AUTH_SECRET=<different-random-secret>
CRON_SECRET=<different-random-secret>
RESEND_API_KEY=<production-resend-key>
EMAIL_FROM=ProBeya <[email protected]>

STRIPE_SECRET_KEY=<production-secret-key>
STRIPE_WEBHOOK_SECRET=<endpoint-signing-secret>
STRIPE_PRICE_STARTER=price_<starter-monthly-id>
STRIPE_PRICE_PRO=price_<pro-monthly-id>

NEXT_PUBLIC_APP_URL=https://probeya.example.com
NEXT_PUBLIC_WS_URL=wss://ws.example.com

URL-encode passwords embedded in connection strings. The runtime URL uses probeya_app; only the schema one-shot service receives the probeya_admin URL.

2

Install certificates and DNS

Provision the application, wildcard tenant, WebSocket, documentation, and file-storage hostnames. Place certificates at the paths documented in docker/README.md and expected by docker/nginx/probeya.conf.

Realtime clients connect directly to NEXT_PUBLIC_WS_URL; the application hostname does not provide a /ws fallback.

3

Validate the merged model

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  config --quiet

Database-first release sequence

Application images are not database administration images. The web image is a minimal Next.js standalone artifact and does not contain the database workspace.

Use this order for a new V2 database and every post-baseline V2 upgrade.

The one-time Phase 143 cutover does not upgrade MVP-v0 in place. Retain any required v0 backup, provision an empty PostgreSQL 16 database or volume, and run the V2 sequence against that target. The migrator rejects a non-empty schema without the immutable V2 ledger.

1

Stop writes and create a backup

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  stop web ws

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  exec -T postgres sh -ec \
    'pg_dump --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --format=custom' \
  > probeya-before-upgrade.dump

Verify that the backup can be restored before continuing. On a brand-new, empty database, start PostgreSQL first; a pre-schema backup is optional only while no application data exists.

2

Adopt schema ownership

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  --profile ownership run --rm ownership

This transactional, idempotent operation reconciles the runtime/admin roles and transfers supported application objects to the non-superuser probeya_admin owner.

3

Apply and verify schema/RLS

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  --profile operations run --build --rm schema

The dedicated image runs db:migrate, apply-rls, and the fail-closed check-rls catalog audit in that order. The migrator verifies immutable journal, SQL, and snapshot hashes, requires an exact-prefix database ledger, takes a PostgreSQL advisory lock, and commits all pending changes in one transaction. --build prevents Compose from reusing a schema image cached from the previous release.

Never run schema commands inside web or ws, and never give DATABASE_ADMIN_URL_DOCKER to either runtime service.

4

Build and start

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  build web ws docusaurus

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  up -d

minio-init must complete successfully before Nginx and web start. It keeps the bucket private and grants anonymous read only to avatar/logo prefixes.

Verify

docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml \
  ps

curl --fail https://probeya.example.com/api/health
curl --fail https://probeya.example.com/api/ready
curl --fail https://ws.example.com/health
curl --fail https://docs.example.com/healthz
EndpointMeaning
web /api/healthProcess liveness
web /api/readyPostgreSQL and Redis readiness
ws /healthWebSocket service health
docusaurus /healthzDocumentation service health

Backups and upgrades

Back up both PostgreSQL and S3_BUCKET, keep copies off-host, and regularly prove restoration in an isolated environment. Attachment objects must remain private during backup and recovery.

Before an upgrade:

git pull --ff-only origin main
pnpm mvp:gate

Then repeat the exact backup -> ownership -> schema -> build -> up -d sequence above. If either one-shot database operation fails, keep application writers stopped and correct/replay the operation or restore the backup.

Other orchestrators

The repository supplies Dockerfile.web, Dockerfile.ws, Dockerfile.docs, and Dockerfile.schema. A different orchestrator must preserve the same role separation, startup order, storage ACL bootstrap, and health checks. Supported Helm charts and Kubernetes manifests are not currently included in this repository.