This guide walks you through running the Comp app and portal with Docker.
- You will run two services:
app(primary) andportal(customer portal). - You must bring your own externally hosted PostgreSQL database. The stack does not run a local DB in production mode.
- You must provide email (Resend) and Trigger.dev credentials for email login and automated workflows.
- Docker Desktop (or Docker Engine) installed
- Externally hosted PostgreSQL 14+ (e.g., DigitalOcean, Neon, RDS) with SSL
- Resend account and API key for transactional email (magic links, OTP)
- Trigger.dev account and project for automated workflows
Set these in docker-compose.yml under each service as shown below.
App (apps/app):
DATABASE_URL(required): External Postgres URL. Example:postgresql://user:pass@host:5432/db?sslmode=requireAUTH_SECRET(required): 32-byte base64. Generate withopenssl rand -base64 32RESEND_API_KEY(required): From Resend dashboardREVALIDATION_SECRET(required): Any random stringBETTER_AUTH_URL(required): Base URL of the app server (e.g.,http://localhost:3000)NEXT_PUBLIC_BETTER_AUTH_URL(required): Same as above for client codeNEXT_PUBLIC_PORTAL_URL(required): Base URL of the portal server (e.g.,http://localhost:3002)TRIGGER_SECRET_KEY(required for workflows): From Trigger.dev project settings- Optional (infrastructure):
UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN
Portal (apps/portal):
DATABASE_URL(required): Same external Postgres URLBETTER_AUTH_SECRET(required): A secret used by portal auth (distinct from appAUTH_SECRET)BETTER_AUTH_URL(required): Base URL of the portal (e.g.,http://localhost:3002)NEXT_PUBLIC_BETTER_AUTH_URL(required): Same as portal base URL for client codeRESEND_API_KEY(required): Same Resend key
App (apps/app):
- OPENAI_API_KEY: Enables AI features that call OpenAI models.
- UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN: Optional Redis (Upstash) used for rate limiting/queues/caching.
- NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST: Client analytics via PostHog; leave unset to disable.
- NEXT_PUBLIC_GTM_ID: Google Tag Manager container ID for client tracking.
- NEXT_PUBLIC_LINKEDIN_PARTNER_ID, NEXT_PUBLIC_LINKEDIN_CONVERSION_ID: LinkedIn insights/conversion tracking.
- NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_LABEL: Google Ads conversion tracking label.
- DUB_API_KEY, DUB_REFER_URL: Dub.co link shortener/referral features.
- FIRECRAWL_API_KEY: Optional LLM/crawling providers for research features.
- SLACK_SALES_WEBHOOK: Slack webhook for sales/lead notifications.
- GA4_API_SECRET, GA4_MEASUREMENT_ID: Google Analytics 4 server/client tracking.
- NEXT_PUBLIC_API_URL: Override client API base URL (defaults to same origin).
Portal (apps/portal):
- NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST: Client analytics via PostHog for portal.
- UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN: Optional Redis if you enable portal-side rate limiting/queues.
We keep docker-compose.yml generic and read values from .env:
services:
migrator:
build:
context: .
dockerfile: Dockerfile
target: migrator
env_file:
- .env
seeder:
build:
context: .
dockerfile: Dockerfile
target: migrator
env_file:
- .env
command: sh -lc "bunx prisma generate --schema=node_modules/@trycompai/db/dist/schema.prisma && bun packages/db/prisma/seed/seed.js"
app:
build:
context: .
dockerfile: Dockerfile
target: app
args:
NEXT_PUBLIC_BETTER_AUTH_URL: ${BETTER_AUTH_URL}
ports: ['3000:3000']
env_file: [.env]
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:3000/api/health || exit 1']
interval: 30s
timeout: 10s
retries: 3
portal:
build:
context: .
dockerfile: Dockerfile
target: portal
args:
NEXT_PUBLIC_BETTER_AUTH_URL: ${BETTER_AUTH_URL_PORTAL}
ports: ['3002:3000']
env_file: [.env]
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:3002/ || exit 1']
interval: 30s
timeout: 10s
retries: 3Create a .env file at the repo root with your values (never commit real secrets):
# External PostgreSQL (required)
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require
# App auth + URLs (required)
AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_PORTAL_URL=http://localhost:3002
REVALIDATION_SECRET=
# Email (required)
RESEND_API_KEY=
# Workflows (Trigger.dev hosted)
TRIGGER_SECRET_KEY=
# Portal auth + URLs (required)
BETTER_AUTH_SECRET=
BETTER_AUTH_URL_PORTAL=http://localhost:3002
NEXT_PUBLIC_BETTER_AUTH_URL_PORTAL=http://localhost:3002
# Optional
# OPENAI_API_KEY=
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=
# NEXT_PUBLIC_POSTHOG_KEY=
# NEXT_PUBLIC_POSTHOG_HOST=
# NEXT_PUBLIC_GTM_ID=
# NEXT_PUBLIC_LINKEDIN_PARTNER_ID=
# NEXT_PUBLIC_LINKEDIN_CONVERSION_ID=
# NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_LABEL=
# DUB_API_KEY=
# DUB_REFER_URL=
# FIRECRAWL_API_KEY=
# SLACK_SALES_WEBHOOK=
# GA4_API_SECRET=
# GA4_MEASUREMENT_ID=
# NEXT_PUBLIC_API_URL=-
migrator: Runs
prisma migrate deployusing the combined schema from@trycompai/db.- Purpose: create/update tables, indexes, and constraints in your hosted Postgres.
- Safe to run repeatedly (Prisma applies only pending migrations).
-
seeder: Generates a Prisma client from the same combined schema and executes the app’s seed script.
- Purpose: load application reference data (frameworks, controls, relations).
- Behavior: idempotent upserts by
id. It does not delete rows; existing rows with matching ids are updated, and relations are connected if missing.
Notes:
- The stack migrates with
@trycompai/dbcombined Prisma schema and then seeds. Seeding is idempotent: records are upserted by id and relations are connected; nothing is deleted. - Ensure your DB user has privileges to create/alter tables in the target database.
Trigger.dev powers AI automations and background workflows.
Steps:
- Create an account at
https://cloud.trigger.dev - Create a project and copy
TRIGGER_SECRET_KEY - From your workstation (not inside Docker):
cd apps/app bunx trigger.dev@latest login bunx trigger.dev@latest deploy - Set
TRIGGER_SECRET_KEYin theappservice environment.
- Create a Resend account and get
RESEND_API_KEY - Add a domain if you plan to send emails from a custom domain
- Set
RESEND_API_KEYin bothappandportalservices
Copy the example and fill real values (kept out of git):
cp .env.example .env
# edit .env with your production secrets and URLsdocker compose down --rmi all --volumes --remove-orphans
docker builder prune --all --forcedocker compose build --no-cachedocker compose run --rm migrator
docker compose run --rm seederdocker compose up -d app portalVerify health:
curl -s http://localhost:3000/api/health- Set real domains and HTTPS (behind a reverse proxy / load balancer)
- Update
BETTER_AUTH_URL,NEXT_PUBLIC_BETTER_AUTH_URL, and portal equivalents to the public domains - Use strong secrets and rotate them periodically
- Ensure the hosted Postgres requires SSL and restricts network access (VPC, IP allowlist, or private networking)