The boilerplate is optimized for Docker deployment with Coolify, featuring multi-stage builds, health checks, and proper environment variable handling.
Multi-Stage Docker
Optimized 3-stage build for minimal image size.
Health Endpoint
DB-probing /api/health for external (Coolify/Traefik) liveness monitoring.
Coolify Optimized
docker-compose.yml configured for Coolify deployment.
Non-Root User
Runs as non-root user for enhanced security.
Docker Architecture
The deployment uses a multi-stage Docker build optimized for production. Here's how each stage works:
| Stage | Purpose | What Happens |
|---|---|---|
| 1. Dependencies | Install npm packages | Uses Node 25 (Debian slim), runs npm install --prefer-offline for a lockfile-aware install. This layer is cached unless package.json changes. |
| 2. Builder | Build the Next.js app | Copies source code, injects NEXT_PUBLIC_* variables as build args, runs npm run build to create standalone output. |
| 3. Runner | Production image | Minimal image with only the built app. Creates non-root user, copies standalone build, exposes port 3777. |
Key Features
- Standalone Output - Next.js creates a self-contained build that doesn't require node_modules at runtime
- Non-Root User - Application runs as
nextjsuser for security (not root) - Health Endpoint -
/api/healthis available for the reverse proxy/orchestrator; the Docker/Compose healthcheck is intentionally disabled (Coolify/Traefik handle liveness) - Debian Slim Base - Minimal
node:25-slimimage keeps the runtime small
Docker Compose (Coolify)
The docker-compose.yml file orchestrates the deployment with these key configurations:
| Configuration | Purpose |
|---|---|
| Build Args | NEXT_PUBLIC_* variables are passed at build time and baked into the image. Changing them requires a rebuild. |
| Environment | Server-only secrets (API keys, webhooks) are injected at runtime. Can be changed without rebuilding. |
| Resource Limits | Default: 2 CPU, 2GB RAM limit with 0.5 CPU, 512MB reservation. Prevents runaway processes. |
| Health Check | Docker healthcheck is disabled (healthcheck: disable: true); Coolify/Traefik perform external monitoring against /api/health. |
| Restart Policy | unless-stopped - Auto-restart on crash, but not if manually stopped. |
| Network | Joins the coolify external network for reverse proxy integration. |
The Dockerfile and docker-compose.yml are located at the root of the project. Review them for the complete configuration.
Production Go-Live Checklist
Run through this once before pointing real users at the deployment:
- Create the production Supabase project; run
supabase/schema.sql(andcms-schema.sql) in the SQL editor; enable thepg_cron+pg_netextensions. - Set the Supabase Auth redirect URLs + Site URL to your domain (
https://your-domain.comandhttps://your-domain.com/*/callback), or magic-link / OAuth will break. - Switch Stripe to live keys, create a live webhook endpoint to
/api/stripe/webhook, select all required events, and copy the livewhsec_intoSTRIPE_WEBHOOK_SECRET. Replace all test price IDs inconfig/pricing.tswith live-mode IDs. - Set every server-only secret in the platform env (not
.env.local): rotateJOBS_SECRET_KEY, setLOGS_SALT/REFERRAL_SALT/AFFILIATES_SALT, all provider keys. - Set
NEXT_PUBLIC_APP_URLto the real domain andNEXT_PUBLIC_INDEXABLE=true; setNEXT_PUBLIC_PRELAUNCH=falsewhen you go public. - Multi-replica only: configure
UPSTASH_REDIS_REST_URL/TOKEN— the in-memory rate-limiter is per-instance and will not coordinate across replicas. KeepTRUST_CLOUDFLARE_IP=falseunless direct origin access is blocked or your trusted proxy strips spoofed client-IP headers. - Register the cron jobs (rows in the
jobstable) and set the Jobs API URL to the public origin; verify they run (not localhost). - Edit the legal CMS pages (
terms,privacy,legal) before launch. - Smoke test:
/api/health→ a real test checkout → magic-link login → an AI message → confirm credits decremented. EnableLOGS_ENABLEDand watch/admin-dashboard/logs. - Enable Supabase automated backups / PITR and confirm a rollback path before your first real customer.
The app is a standard Next.js standalone build, so it deploys to Vercel without the Dockerfile — set the same env vars in the project settings. Caveat: pg_cron still calls your public /api/jobs/run URL, so jobs work the same; just point the Jobs API URL at the Vercel domain.
Environment Variables
In production, set all server-only secrets at the platform level (Coolify/Vercel project env), never in a committed .env.local: SUPABASE_SECRET_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (the live one), JOBS_SECRET_KEY, and the hash salts (LOGS_SALT, REFERRAL_SALT, AFFILIATES_SALT). Build-time NEXT_PUBLIC_* values must be present at build, not just runtime. TRUST_CLOUDFLARE_IP defaults to false; set it to true only when the origin is protected by Cloudflare or a trusted proxy that strips spoofed headers. The full env contract lives in .env.example; the ordered Production Go-Live Checklist is above.
NEXT_PUBLIC_* variables are embedded at build time and require a rebuild to change. Server-only variables can be changed at runtime.
Required Variables
These variables must be set for the application to function. Copy .env.example to .env.local and fill in values for: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY (from your Supabase project settings), STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (from your Stripe dashboard), LEGAL_COMPANY_NAME, LEGAL_ADDRESS, LEGAL_REGISTRATION_NUMBER for public legal pages, and at least one AI provider key (OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_AI_API_KEY).
Email Configuration
Set EMAIL_PROVIDER to brevo, mailjet, or noop. Provide the matching credentials (BREVO_API_KEY for Brevo, or MAILJET_API_KEY_PUBLIC + MAILJET_API_KEY_PRIVATE for Mailjet) plus EMAIL_FROM_NAME and EMAIL_FROM_ADDRESS (the verified sender email). The active provider handles transactional emails (magic links, invitations, deletion notifications, license expiration warnings) and newsletter subscriptions.
Optional Integrations
Optional variables enable additional features: NEXT_PUBLIC_GTM_ID for Google Tag Manager analytics, NEXT_PUBLIC_META_PIXEL_ID for Facebook/Meta pixel tracking, NEXT_PUBLIC_CRISP_WEBSITE_ID for live chat support, UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN for production rate limiting (falls back to in-memory without these), and VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY (server-only) for PWA push notifications. Analytics/chat scripts remain consent-gated even when configured.
Health Check Endpoint
The application includes a health check endpoint at /api/health used by Docker and monitoring tools.
| Field | Description |
|---|---|
status |
"ok" (HTTP 200) or "degraded" (HTTP 503 when the DB probe fails) |
db |
"ok" / "error" / "timeout" |
duration_ms |
DB probe duration in milliseconds |
error |
Error message (only present when degraded) |
The Docker/Compose healthcheck is disabled by design; point your reverse proxy or platform liveness probe at /api/health (it returns 503 when the database probe fails).
Coolify Deployment Steps
Follow these steps to deploy on Coolify:
- Create Application - In Coolify, create a new application from your Git repository. Select "Docker Compose" as the build pack.
- Configure Build Args - Add all
NEXT_PUBLIC_*variables in the Build Args section. These are embedded at build time. - Configure Environment - Add server-only secrets (API keys, webhook secrets) in the Environment section. These can be changed without rebuilding.
- Set Domain - Add your domain in Coolify. SSL certificates are automatically provisioned via Let's Encrypt.
- Deploy - Click "Deploy" and wait for the build to complete (typically 3-5 minutes for first build, faster for subsequent builds due to caching).
- Verify - Check
https://your-domain.com/api/healthreturns a healthy status.
Resource Recommendations
| Tier | CPU | Memory | Use Case |
|---|---|---|---|
| Development | 1 CPU | 1 GB | Local testing, staging |
| Production (Small) | 2 CPU | 2 GB | Low traffic, startup |
| Production (Medium) | 4 CPU | 4 GB | Medium traffic, growing |
| Production (Large) | 8 CPU | 8 GB | High traffic, enterprise |
Note: Build requires more resources than runtime. Allow 4 GB memory during build process.
Stripe Webhook Setup
Stripe webhooks notify your application when payment events occur (subscriptions, payments, cancellations).
- Create Webhook - In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint
- Set Endpoint URL - Use
https://your-domain.com/api/stripe/webhook - Select Events - Enable these events:
- Checkout Events:
checkout.session.completed- Purchases (subscriptions, credit packs, licenses)checkout.session.async_payment_succeeded- Async payment completed (SEPA, bank transfer)checkout.session.async_payment_failed- Async payment failedcheckout.session.expired- Abandoned checkout tracking- Subscription Events:
customer.subscription.created- New subscriptionscustomer.subscription.updated- Plan changes, status updatescustomer.subscription.deleted- Cancellationscustomer.subscription.paused- Subscription pausedcustomer.subscription.resumed- Subscription resumedcustomer.subscription.trial_will_end- Trial ending notification (3 days before)- Invoice Events:
invoice.paid- Successful payments / monthly credit refillsinvoice.payment_failed- Failed payments (grace period)invoice.payment_action_required- SCA/3D Secure authentication required- Charge & Dispute Events:
charge.refunded- Refunds (deducts credits, revokes licenses)charge.dispute.created- Chargeback created (requires immediate action)charge.dispute.closed- Dispute resolved (won/lost)- Customer Events:
customer.created- Link Stripe customer to accountcustomer.updated- Sync customer data changescustomer.deleted- Clean up Stripe references- Payment Method Events:
payment_method.attached- New payment method addedpayment_method.detached- Payment method removed (churn signal)
- Copy Signing Secret - After creation, copy the webhook signing secret (starts with
whsec_) - Add to Environment - Set
STRIPE_WEBHOOK_SECRETin your environment variables
Use the Stripe CLI to forward webhooks locally: stripe listen --forward-to localhost:3777/api/stripe/webhook
Supabase Database Setup
Supabase provides the PostgreSQL database, authentication, and file storage for the application.
- Create Project - Create a new project in the Supabase dashboard
- Run Schema Migration - Copy the contents of
supabase/schema.sqland run it in the SQL Editor - Enable Extensions - Enable these PostgreSQL extensions:
uuid-ossp- UUID generation (usually enabled by default)pg_cron- Scheduled job execution (for background jobs)pg_net- HTTP requests from database (for job triggers)
- Configure Authentication - In Auth settings:
- Enable Email provider for magic links
- Add redirect URLs for your domain
- Optionally configure OAuth providers (Google, GitHub)
- Create Storage Bucket - Create a public bucket named
cms-mediafor CMS uploads (automatically created bynpm run init) - Copy API Keys - From Project Settings → API:
- Project URL →
NEXT_PUBLIC_SUPABASE_URL - Publishable key →
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY - Secret key →
SUPABASE_SECRET_KEY
- Project URL →