Passa al contenuto principale

Docker Deployment

The supported self-hosted deployment uses the base Compose file together with the production overlay. Always pass both files, in this order:

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

The overlay extends the local infrastructure model; it is not a standalone Compose file.

Production Topology​

ServiceContainer portRole
nginx80, 443Public TLS entry point and hostname routing
web8000Next.js application, tRPC API, and background outbox worker
ws8003WebSocket collaboration service
docusaurus3001Generated documentation site
postgres5432PostgreSQL 16 database
redis6379Cache and pub/sub
minio9000, 9001S3-compatible object storage and console
clamav3310Fail-closed attachment scanning
minio-initone shotPrivate bucket and least-privilege application user bootstrap
ownershipone shotExisting-schema ownership adoption
schemaone shotDrizzle schema, reviewed SQL, and RLS verification

Only Nginx publishes public ports. The database, Redis, MinIO, and ClamAV host bindings in the production overlay are loopback-only. There is no separate API container: the tRPC and HTTP routes run inside web.

MVP Support Boundary​

This production profile supports the tenant-aware authenticated product core, public forms, realtime board collaboration, and attachments through the exact tenant-aware tRPC boundary. Nginx returns 404 for cron handlers, GraphQL, REST v1, MCP, direct PDF exports, API documentation, enterprise SSO/SAML, MQTT ingest, and the ADO/Jira/MES inbound webhooks. The separately signed Stripe webhook remains enabled. Authentik and Azure AD are hard-disabled in the production build and runtime. Public board/embed/breakout/link sharing, external approval links, and global template galleries remain outside the MVP support contract. Do not bypass this boundary by giving web an admin database connection.

Prerequisites​

  • Docker 24+ with Docker Compose v2
  • At least 4 CPU cores, 8 GB RAM, and persistent storage for production
  • DNS for the application, WebSocket, docs, and file-storage hostnames
  • TLS certificates at the paths listed in docker/README.md
  • A tested PostgreSQL backup and restore procedure

Configure the Environment​

Copy the checked-in template and replace every placeholder:

cp .env.example .env

At minimum, configure the three distinct database credentials, Redis password, distinct MinIO root and application credentials, authentication secrets, Resend, and the public application/WebSocket/file URLs. Passwords embedded in connection strings must be URL-encoded.

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

DATABASE_URL_DOCKER=postgresql://probeya_app:<encoded-application-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

DATABASE_URL_DOCKER is the RLS-enforced runtime connection. Only the one-shot schema service receives DATABASE_ADMIN_URL_DOCKER. Never give the admin URL to web or ws.

S3_ENDPOINT is reachable from containers. S3_PUBLIC_URL must be reachable from user browsers. Attachments remain private; only explicit avatar and logo prefixes receive anonymous read access.

Validate interpolation before changing a running deployment:

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

First Deployment and Upgrades​

Database changes are intentionally operator-run. The application image is a minimal Next.js standalone image and does not contain the database workspace or CLI. Do not run schema commands inside web.

Use this order for a new V2 installation and every post-baseline V2 upgrade that changes packages/db.

The one-time Phase 143 cutover does not support an in-place MVP-v0 upgrade. 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 application writes. On an upgrade, stop the two writers while keeping PostgreSQL available:

    docker compose \
    -f docker/docker-compose.yml \
    -f docker/docker-compose.prod.yml \
    stop web ws
  2. Create a restorable backup before changing ownership or schema:

    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

    On a brand-new installation, start PostgreSQL first; the backup is optional only while the database is still empty.

  3. Adopt ownership and reconcile the two least-privilege database roles:

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

    This operation is transactional, fail-closed, and safe to replay on a new or existing PostgreSQL 16 database.

  4. Apply and verify the schema:

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

    The schema image runs db:migrate as probeya_admin, then apply-rls and the fail-closed check-rls catalog audit. The migrator verifies immutable journal, SQL, and snapshot hashes, requires the database ledger to be an exact prefix, serializes operators with an advisory lock, and commits all pending changes in one transaction. --build prevents a cached image from the previous release applying stale database code.

  5. Build and start the application stack:

    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

The minio-init service must complete before Nginx and web start. It keeps the bucket private, creates the application user, and can be replayed during credential rotation.

Verification​

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

/api/health is the web process liveness probe. /api/ready verifies both PostgreSQL and Redis and is used by the container health check.

Run the release gate before promotion:

pnpm mvp:gate

Logs and Recovery​

# All services
docker compose \
-f docker/docker-compose.yml \
-f docker/docker-compose.prod.yml \
logs -f

# Application writers only
docker compose \
-f docker/docker-compose.yml \
-f docker/docker-compose.prod.yml \
logs -f web ws

If ownership or schema fails, keep web and ws stopped, inspect the failure, and either correct it and replay the failed one-shot operation or restore the backup. Never bypass a failed RLS verification to bring the application online.

For the canonical credential, certificate, object-storage, and validation details, see docker/README.md.