Docker
The project includes a production-ready multi-stage Dockerfile and a docker-compose.yml.
Build the Image
docker build -t buildpad-daas:latest .Run with Docker Compose
Copy .env.local.example to .env and fill in your Supabase credentials, then:
docker-compose up -dThe app is available at http://localhost:3000.
Environment Variables
Pass environment variables via a .env file or directly:
docker run -d \
-p 3000:3000 \
-e NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co \
-e NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \
-e SUPABASE_SERVICE_ROLE_KEY=your-service-role-key \
buildpad-daas:latestIn Docker/Kubernetes environments where Supabase runs as a sidecar or internal service, use SUPABASE_INTERNAL_URL so the server connects directly without going through the public load balancer:
docker run -d \
-p 3000:3000 \
-e NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co \
-e SUPABASE_INTERNAL_URL=http://supabase-kong:8000 \
-e NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \
-e SUPABASE_SERVICE_ROLE_KEY=your-service-role-key \
buildpad-daas:latestThe browser still uses NEXT_PUBLIC_SUPABASE_URL. All server-side code — including cookie-based session requests — uses SUPABASE_INTERNAL_URL. Cookie names are automatically kept in sync with NEXT_PUBLIC_SUPABASE_URL so browser sessions work correctly.
Shared Cache (Valkey)
Optional. Each worker process caches permission, auth-status, and schema lookups in memory. Point the app at Valkey (or another Redis-compatible server) and those lookups are shared across workers and containers, so a cold process doesn’t reload them from the database.
docker run -d \
-p 3000:3000 \
-e NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co \
-e NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \
-e SUPABASE_SERVICE_ROLE_KEY=your-service-role-key \
-e VALKEY_URL=redis://valkey:6379 \
buildpad-daas:latest| Variable | Default | Purpose |
|---|---|---|
VALKEY_URL | — | redis:// or rediss:// (TLS) URL. Comma-separate seed nodes in cluster mode. |
VALKEY_MODE | standalone, or sentinel when VALKEY_SENTINEL_NODES is set | standalone, cluster, or sentinel. |
VALKEY_SENTINEL_NODES | — | host:port list. The app asks the sentinels for the current master. |
VALKEY_SENTINEL_MASTER | mymaster | Master name the sentinels monitor. |
VALKEY_USERNAME, VALKEY_PASSWORD | — | Credentials. Override any in VALKEY_URL. |
VALKEY_TLS | false | Force TLS. |
VALKEY_KEY_PREFIX | daas | Key namespace. Give each environment that shares one Valkey its own prefix. |
VALKEY_SESSION_CACHE_TTL_MS | 0 (off) | Remember access tokens GoTrue accepted, so repeat requests skip the GoTrue round trip. |
Under Compose the bundled server is opt-in: add VALKEY_URL=redis://valkey:6379 to your .env and start the profile.
docker compose --profile valkey up -dWithout Valkey, or while it is unreachable, the app runs on its in-memory caches and retries the connection every VALKEY_RETRY_MS (default 30 s). Invalidation stays immediate: a permission, user, or schema change also retires the shared entries.
With VALKEY_SESSION_CACHE_TTL_MS set, a session revoked outside this app — through the GoTrue admin API, or by a password change in another client — keeps working here for up to that TTL. Logout through the app, user suspension, and MFA checks still take effect immediately. Keep the TTL short, for example 60000.
Startup Entrypoint
The docker-entrypoint.sh script runs database migrations automatically on container start before starting the cluster launcher (cluster.js). This ensures the schema is always up to date without manual intervention.
Migrations are idempotent — running them on a database that’s already up to date is safe.
Multi-Process Cluster
The image starts node cluster.js (not node server.js). The launcher forks one worker per available core; workers share port 3000 and the primary distributes connections between them. Set WEB_CONCURRENCY to cap the worker count.
Each worker is a full Next.js server (~400 MB resident under load), so size container memory at roughly workers × 400–500 MB. If the container’s memory limit is below cores × 400 MB, set WEB_CONCURRENCY explicitly — otherwise the default forks more workers than the limit can hold and the container OOM-loops on startup (e.g., a 1 GB limit → WEB_CONCURRENCY=2).
Crashed workers restart automatically, and the cron scheduler runs on a single leader worker with failover if that worker dies.
Known limitation: the in-memory log buffer behind the Logs page is per worker, so /logs shows only one worker’s output.
Health Check
The Docker Compose configuration includes a health check that polls /api/health every 30 seconds.
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', ...)"]
interval: 30s
timeout: 3s
retries: 3Building the Docs Site
The documentation site is built into the image automatically (Dockerfile stage docs-builder) and served by the app at /docs. The separate nginx docs service in docker-compose.yml (profile standalone-docs, port 3001) is only needed to serve the docs as a standalone site — see AWS Deployment.