25 KiB
Deploying SpomeniQR to Coolify — Final Production Reference
This document is the complete, verified guide for deploying SpomeniQR
(a Next.js multi-tenant memorial app) to a VPS managed by
Coolify. It reflects the working production setup for
testbed.mk including wildcard subdomains, a manual wildcard TLS certificate
issued via acme.sh + Contabo DNS, and the Traefik coolify-proxy that Coolify
runs.
It supersedes docs/coolify.md and docs/deploy.md for the Coolify target.
Table of Contents
- Working stack snapshot
- Prerequisites
- DNS configuration
- Install Coolify
- Create the project & database
- Add the application
- Environment variables
- Domain & subdomain routing (Traefik labels)
- Subdomain certificates (wildcard TLS)
- Deploy the application
- Verify everything
- Troubleshooting
- Maintenance & commands
1. Working stack snapshot
The production composition that is known to work:
| Piece | Value |
|---|---|
| Proxy | Coolify's Traefik (service coolify-proxy) |
| Proxy image | traefik:v3.6.x (must be v3.6+, see §12.1 — a plain v3.1 is broken) |
| App container | built from this repo's Dockerfile (Next.js standalone), listens on :3000 |
| Database | Coolify-managed PostgreSQL 16, hostname spomeniqr-db |
| Wildcard routing | Traefik HostRegexp labels on the app container (v3 syntax) |
| Wildcard TLS | acme.sh + Contabo DNS-01 cert for testbed.mk + *.testbed.mk, served as Traefik default certificate |
| Subdomain logic | handled in-app from the Host header (src/middleware.ts) — no X-Subdomain proxy header needed |
Internet
│
├── testbed.mk ─────────────► Traefik (coolify-proxy) ──► app:3000
│ │
└── *.testbed.mk ──────────────────────┤ (HostRegexp label)
└─► serves wildcard default cert (acme.sh)
The app derives the memorial subdomain directly from the request Host header
(getSubdomain() in src/middleware.ts), rewrites / → /<subdomain>, and
renders src/app/[subdomain]/page.tsx. The proxy only needs to route every
host under testbed.mk to the app and terminate TLS — it does not need to set
any custom header.
2. Prerequisites
- A VPS running a recent Linux (Ubuntu 22.04+ used here), ≥2 GB RAM.
- Docker 29.x is what triggers the critical Traefik compatibility step
below. If your VPS only has Docker ≤28, Traefik
v3.1still works, but the production setup documented here assumes Docker 29 and pins Traefikv3.6. - Domain
testbed.mkregistered, with DNS nameserver access (Contabo DNS in this project, used by the wildcard cert's DNS-01 challenge). - A Contabo S3 bucket (
monuments-images) with public-read policy (seedocs/deploy.md§4) and a Clerk production instance.
3. DNS configuration
Create two DNS records at your provider:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ |
YOUR_VPS_IP |
300 |
| A | * |
YOUR_VPS_IP |
300 |
@makestestbed.mkresolve → VPS.*(wildcard) makes every*.testbed.mkresolve → VPS. Required for tenant subdomains and for the wildcard cert's DNS-01 ACME validation domain (_acme-challenge.testbed.mk).
Verify:
dig testbed.mk +short
dig random.testbed.mk +short # both must print YOUR_VPS_IP
The DNS-01 challenge for a wildcard cert needs an
_acme-challenge.testbed.mkTXT record which the DNS client creates automatically via API. The Contabo API credentials indns_contabo.shmust therefore have permission to managetestbed.mkrecords.
4. Install Coolify
Use the official installer (coolify.io/docs/installation) or the Vagrant-free
server method. On a fresh VPS:
curl -fsSL https://coolify.io/install | bash
Coolify will install Docker and its own stack (coolify, coolify-db,
coolify-redis, coolify-realtime, coolify-sentinel, coolify-proxy, …).
After installation it manages:
- its proxy configuration under
/data/coolify/proxy/ - its application source under
/data/coolify/source/
Finish the installer in the browser and log in to the dashboard.
5. Create the project & database
- Dashboard → + Add New Project → name it
SpomeniQR. - Inside the project → + Add New Resource → Database → PostgreSQL:
- Name:
spomeniqr-db - PostgreSQL version: 16
- Database name:
monuments - Username:
postgres - Password: strong, unique
- Name:
- Deploy. Copy the Internal Connection String, e.g.:
postgresql://postgres:PASSWORD@spomeniqr-db:5432/monuments(the host is the Coolify-internal service namespomeniqr-db, notlocalhost).
6. Add the application
- Inside the project → + Add New Resource → Application.
- Select Public Repository (or Private with a deploy key).
- Name:
spomeniqr - Repository URL: your Git URL
- Branch:
main - Build Pack: Docker (uses the repo's
Dockerfile; verified path).
- Name:
- Wire the database to the app on the shared Coolify network (
quick connectbutton or ensure both are oncoolifynetwork).
The repo Dockerfile already handles prisma generate, next build,
prisma migrate deploy, and provisioning the super admin on startup, so no
custom build/start commands are needed.
7. Environment variables
In the app's Environment Variables panel add (mark every NEXT_PUBLIC_*
as a Build Variable):
# Database — Coolify internal hostname
DATABASE_URL=postgresql://postgres:PASSWORD@spomeniqr-db:5432/monuments
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
NEXT_PUBLIC_CLERK_FAPI_HOST=clerk.testbed.mk # build var, only if using custom frontend API domain
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
# Contabo S3
S3_ENDPOINT=https://eu2.contabostorage.com
S3_REGION=eu-2
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_BUCKET_NAME=monuments-images
# App
NEXT_PUBLIC_APP_URL=https://testbed.mk
NEXT_PUBLIC_APP_DOMAIN=testbed.mk
# Admin session signing secret (openssl rand -hex 32)
ADMIN_SESSION_SECRET=your-random-64-char-secret
# Super-admin (bcrypt hash, NOT plaintext)
SUPER_ADMIN_USERNAME=super
SUPER_ADMIN_PASSWORD_HASH=$2b$12$REPLACE_WITH_BCRYPT_HASH
# Node
NODE_ENV=production
Critical rules
NEXT_PUBLIC_*variables are inlined atnext build. Changing one requires a redeploy (not just a restart).NEXT_PUBLIC_CLERK_FAPI_HOSTin particular must be a build var or the build-time CSP blocks Clerk JS.SUPER_ADMIN_PASSWORD_HASHis passed literally by Coolify — paste the raw$2b$12$…hash with no$$escaping (that escaping is only forenv_fileCompose usage). Generate with:node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))"- Server-only vars (
CLERK_SECRET_KEY,DATABASE_URL,ADMIN_SESSION_SECRET,SUPER_ADMIN_PASSWORD_HASH,S3_*) stay as normal runtime vars. ADMIN_SESSION_SECRETmissing ⇒ super-admin login returns 500 at boot.
8. Domain & subdomain routing (Traefik labels)
Coolify's proxy in this project is Traefik. Routing to the app is defined by
Docker labels in the repo's docker-compose.yaml, which Coolify applies
when it deploys the container. Keep that file in sync in the repo:
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- .env
networks:
- coolify
labels:
- traefik.enable=true
- traefik.docker.network=coolify
- traefik.http.routers.testbed-root.rule=Host(`testbed.mk`)
- traefik.http.routers.testbed-root.entryPoints=https
- traefik.http.routers.testbed-root.service=spomeniqr-svc
- traefik.http.routers.testbed-root.tls=true
- traefik.http.routers.testbed-wildcard.rule=HostRegexp(`^[a-z0-9-]+\.testbed\.mk$$`)
- traefik.http.routers.testbed-wildcard.entryPoints=https
- traefik.http.routers.testbed-wildcard.service=spomeniqr-svc
- traefik.http.routers.testbed-wildcard.tls=true
- traefik.http.routers.testbed-root-http.rule=Host(`testbed.mk`)
- traefik.http.routers.testbed-root-http.entryPoints=http
- traefik.http.routers.testbed-root-http.middlewares=redirect-to-https
- traefik.http.routers.testbed-root-http.service=spomeniqr-svc
- traefik.http.routers.testbed-wildcard-http.rule=HostRegexp(`^[a-z0-9-]+\.testbed\.mk$$`)
- traefik.http.routers.testbed-wildcard-http.entryPoints=http
- traefik.http.routers.testbed-wildcard-http.middlewares=redirect-to-https
- traefik.http.routers.testbed-wildcard-http.service=spomeniqr-svc
- traefik.http.services.spomeniqr-svc.loadbalancer.server.port=3000
- traefik.http.middlewares.gzip.compress=true
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
networks:
coolify:
external: true
Traefik v3 HostRegexp — critical syntax note
- ✅ v3-valid:
HostRegexp(^[a-z0-9-]+.testbed.mk$) - ❌ v2 (removed in v3):
HostRegexp({subdomain:[a-zA-Z0-9-]+}.testbed.mk)
The v2 named-capture form silently fails to parse under Traefik 3, so the wildcard router is not created and subdomains fall through to Coolify's default 503 page ("server not available") even though the apex works. Do not reintroduce the v2 form.
In docker-compose.yaml the trailing $ anchor must be written as $$ so YAML
interpolation yields a literal $. After editing, the container must be
recreated for the new labels to take effect (a plain restart is not enough).
Effect of the two HTTPS routers
Host(\testbed.mk`)` → apex landing page.HostRegexp(\^[a-z0-9-]+.testbed.mk$`)→ **every** subdomain (perop,test-memorial, …) → sameapp:3000`. The app's middleware then maps the subdomain to a memorial page.- Both are
tls=truewithout a certresolver, so Traefik uses the default certificate — which is the wildcard cert from §9 Subdomain certificates. Notls.certresolverlabel is needed on these routers.
Because Coolify's own default routers can also be generated from the UI
Domainspanel, prefer addingtestbed.mk/*.testbed.mkthere or relying solely on these labels — avoid having both apply the same rule, which creates duplicate-router ambiguity.
9. Subdomain certificates (wildcard TLS)
This section is dedicated to provisioning and renewing the TLS certificate that
secures all *.testbed.mk subdomains, since it is the part that is easiest
to get wrong.
9.1 Why a wildcard cert
The app issues an arbitrary number of tenant subdomains
(<subdomain>.testbed.mk). It is impractical to obtain an individual
certificate per subdomain. A single wildcard certificate for *.testbed.mk
covers every present and future tenant. Because it is issued as Traefik's
default certificate, any router with tls=true and no explicit
certresolver (which is exactly how §8 is configured) is served the wildcard
cert automatically.
9.2 Challenge type & provider
Wildcard certs can only be validated with the DNS-01 challenge (an
_acme-challenge.testbed.mk TXT record — there is no HTTP path to validate
.testbed.mk itself). This project uses acme.sh with a custom Contabo DNS
client (dns_contabo.sh) because the domain is hosted on Contabo DNS.
9.3 Required credentials
Obtain from your Contabo customer account:
| Variable | Purpose |
|---|---|
CONTABO_CLIENT_ID |
OAuth2 Client ID |
CONTABO_CLIENT_SECRET |
OAuth2 Client Secret |
CONTABO_API_USER |
Contabo account email |
CONTABO_API_PASSWORD |
Contabo account password |
dns_contabo.sh reads these from the environment / acme.sh account conf and
uses Contabo's API (https://api.contabo.com/v1) to create and delete the
_acme-challenge TXT records needed for validation.
9.4 Install dns_contabo.sh
- Install
acme.sh(non-root or root):curl https://get.acme.sh | sh -s email=you@example.com - Place
dns_contabo.shwhere acme.sh finds API plugins. It is written to be self-contained (usesacme.shbuilt-ins_post/_info/_err), so the cleanest approach is:mkdir -p ~/.acme.sh/dnsapi cp /path/to/dns_contabo.sh ~/.acme.sh/dnsapi/ chmod +x ~/.acme.sh/dnsapi/dns_contabo.sh
9.5 Export credentials
export CONTABO_CLIENT_ID="..."
export CONTABO_CLIENT_SECRET="..."
export CONTABO_API_USER="you@example.com"
export CONTABO_API_PASSWORD="..."
9.6 Issue the wildcard certificate
# Covers the apex AND every subdomain
~/.acme.sh/acme.sh --issue \
--dns dns_contabo \
-d testbed.mk \
-d '*.testbed.mk'
acme.sh stores the result in ~/.acme.sh/testbed.mk_ecc/:
fullchain.cer, testbed.mk.key, testbed.mk.cer, ca.cer.
Validation: the
${DOMAIN}_eccdirectory must containtestbed.mk.cerand a key. If verification fails, check the DNS TXT records (dig +short _acme-challenge.testbed.mk TXT) and that the Contabo credentials have record permissions on the zone.
9.7 Traefik default-certificate config (Coolify proxy)
The proxy (coolify-proxy, image traefik:v3.6) mounts the host directory
/data/coolify/proxy/ at /traefik (see §12.2).
Two things must be provisioned there:
a) Copy the cert files into Coolify's certs directory:
COOLIFY_CERT_DIR="/data/coolify/proxy/certs"
mkdir -p "$COOLIFY_CERT_DIR"
cp ~/.acme.sh/testbed.mk_ecc/fullchain.cer "$COOLIFY_CERT_DIR/testbed.mk.cert"
cp ~/.acme.sh/testbed.mk_ecc/testbed.mk.key "$COOLIFY_CERT_DIR/testbed.mk.key"
chmod 644 "$COOLIFY_CERT_DIR/testbed.mk.cert"
chmod 600 "$COOLIFY_CERT_DIR/testbed.mk.key"
b) Create the dynamic config /data/coolify/proxy/dynamic/testbed.mk-wildcard.yaml
(loaded by the proxy's file provider; note paths are inside the container,
i.e. under /traefik/…):
tls:
certificates:
- certFile: /traefik/certs/testbed.mk.cert
keyFile: /traefik/certs/testbed.mk.key
stores:
default:
defaultCertificate:
certFile: /traefik/certs/testbed.mk.cert
keyFile: /traefik/certs/testbed.mk.key
The defaultCertificate under tls.stores.default makes this the cert Traefik
serves for any TLS router that has no explicit certresolver — including both
testbed-root and testbed-wildcard (§8). A change here requires a proxy
reload (restarting coolify-proxy picks it up).
9.8 Install with acme.sh for auto-renewal
Register the copy + reload steps so acme.sh performs them automatically every renewal (certs expire every ~90 days):
~/.acme.sh/acme.sh --install-cert -d testbed.mk -d '*.testbed.mk' \
--fullchain-file "/data/coolify/proxy/certs/testbed.mk.cert" \
--key-file "/data/coolify/proxy/certs/testbed.mk.key" \
--reloadcmd "docker restart coolify-proxy"
--fullchain-file/--key-filetell acme.sh to copy the renewed certs into Coolify's certs directory on every renewal.--reloadcmdrestarts the proxy so Traefik reloads the files.- acme.sh installs its own cron job for renewal, so no separate crontab is needed.
Optionally, a dedicated renewal hook script can be used (kept in
~/.acme.sh/renewal-hooks/testbed.mk-coolify-reload.sh) that copies both files
and runs docker restart coolify-proxy; advantages are clearer logging and a
manual script you can run to force a reload after a manual cert update.
9.9 Verify the wildcard cert is live
# SANs must list both testbed.mk and *.testbed.mk
openssl x509 -in /data/coolify/proxy/certs/testbed.mk.cert -noout -text | grep -A2 "Subject Alternative Name"
# Traefik default cert actually offered on a subdomain
echo | openssl s_client -connect perop.testbed.mk:443 -servername perop.testbed.mk 2>/dev/null \
| openssl x509 -noout -subject -issuer
# Next renewal date
~/.acme.sh/acme.sh --list | grep testbed.mk
9.10 Renewal policy summary
| Item | Value |
|---|---|
| Issuer | Let's Encrypt via acme.sh (DNS-01, dns_contabo) |
| Covered | testbed.mk + *.testbed.mk |
| Lifetime | 90 days; auto-renewed by acme.sh cron |
| Destination | /data/coolify/proxy/certs/testbed.mk.{cert,key} |
| Reload | --reloadcmd docker restart coolify-proxy |
| Served as | Traefik default certificate (tls.stores.default.defaultCertificate) |
Do NOT rely on Coolify's built-in Let's Encrypt
letsencryptcertresolver for the wildcard. Coolify's default uses an HTTP-01 challenge, which cannot validate a wildcard, and Traefik v3 does not auto-provision per-subdomain certs fromHostRegexprouters. The manual DNS-01 wildcard + default-cert approach is the correct one for this app.
10. Deploy the application
- Ensure
[§7 env vars]are saved and allNEXT_PUBLIC_*are marked as build variables before the first build. - Click Deploy. Watch the build log to confirm, in order:
- dependencies installed,
npx prisma generate,next build,npx prisma migrate deploy,[seed] Super-admin 'super' provisioned.,- server listening on port 3000.
- If the routing (Traefik) labels changed (e.g. §8 edits in the repo), triggers a recreate so the new labels apply.
11. Verify everything
# Apex
curl -s -o /dev/null -w "%{http_code}\n" https://testbed.mk # 200
# Subdomain / wildcard routing + default cert
curl -s -o /dev/null -w "%{http_code}\n" https://perop.testbed.mk # 200
# TLS on a subdomain uses the wildcard cert (no browser error)
echo | openssl s_client -connect perop.testbed.mk:443 -servername perop.testbed.mk 2>/dev/null \
| openssl x509 -noout -subject
# Proxy healthy + no docker-provider errors
docker logs coolify-proxy --since 1m 2>&1 | grep -iE "too old|error" | tail
# API health
curl -s https://testbed.mk/api/check-subdomain?slug=test | python3 -m json.tool
# Admin login (expects {"success":true})
curl -s -X POST https://testbed.mk/api/admin/login -H "Content-Type: application/json" \
-d '{"username":"super","password":"YOUR_PASSWORD"}'
Browser checks
https://testbed.mk→ landing page.- Publish a memorial with subdomain
perop, thenhttps://perop.testbed.mk→ the memorial page with a green lock (no certificate warning). - DevTools → Network: no CSP violation for
clerk.testbed.mk/npm/...when the custom Clerk frontend domain is used.
12. Troubleshooting
12.1 Traefik cannot talk to Docker 29 ("client version 1.24 is too old")
Symptom: coolify-proxy logs repeat:
ERR Failed to retrieve information of the docker client ...
error="Error response from daemon: client version 1.24 is too old.
Minimum supported API version is 1.40, please upgrade your client"
providerName=docker
and every domain (apex + subdomains) returns 503.
Cause: Docker 29 raised its minimum API version (here 1.40); Traefik
v3.1 runs a Docker client pinned to API 1.24 and never negotiates, so the
Docker provider — which discovers the app's label routers — is dead. It is not a
config problem. Coolify's default proxy (image traefik:v3.1) hits this on a
Docker 29 host.
Fix: pin the proxy image to Traefik v3.6+ (adds Docker API auto-
negotiation). The proxy compose lives at /data/coolify/proxy/docker-compose.yml
(the service is named traefik):
sed -i "s@traefik:v3.1@traefik:v3.6@" /data/coolify/proxy/docker-compose.yml
grep 'image:' /data/coolify/proxy/docker-compose.yml
docker compose -f /data/coolify/proxy/docker-compose.yml up -d --force-recreate traefik
docker exec coolify-proxy traefik version # expect 3.6.x
Confirm the provider is healthy and routing resumes:
docker logs coolify-proxy --since 30s 2>&1 | grep -i "too old" # should be empty
curl -s -o /dev/null -w "%{http_code}\n" https://testbed.mk # 200
A future Coolify update may regenerate the proxy compose back to v3.1.
Re-apply the sed above after any Coolify update, or (as a durable stopgap)
lower the daemon's minimum API so the 1.24 client is accepted by adding
"min-api-version": "1.24" to /etc/docker/daemon.json and restarting Docker:
# /etc/docker/daemon.json: { ..., "min-api-version": "1.24" }
systemctl restart docker
Prefer the v3.6 image fix — it touches only the proxy, not the whole daemon.
12.2 Proxy mount layout
coolify-proxy mounts the host directory /data/coolify/proxy/ at /traefik.
Everything the proxy reads must therefore be expressed with in-container paths:
- cert files inside the container:
/traefik/certs/* - dynamic config inside the container:
/traefik/dynamic/* - ACME store:
/traefik/acme.json
Verify the mount and files:
docker inspect coolify-proxy --format '{{json .Mounts}}'
docker exec coolify-proxy ls -la /traefik/certs /traefik/dynamic
12.3 Subdomain shows "server not available" (503) while apex works
- Most common: v2
HostRegexplabel. Ensure bothtestbed-wildcardandtestbed-wildcard-httpuse the v3 formHostRegexp(\^[a-z0-9-]+.testbed.mk$$`)` (see §8). Recreate the container after editing labels. - Docker 29 / Traefik v3.1: see §12.1.
- DNS:
dig perop.testbed.mk +shortmust return the VPS IP. - Duplicate routers: if you also added
*.testbed.mkin Coolify's UIDomains, you may have overlapping routers — prefer one method.
12.4 Subdomain loads but shows a certificate error
- Wildcard default cert not loaded / stale. Check §9.7 layout and paths, then
docker restart coolify-proxy. - Cert files not renewed: rerun
~/.acme.sh/acme.sh --renew -d testbed.mk -d '*.testbed.mk' --forceand check the SANs (must include*.testbed.mk).
12.5 Build fails with Prisma errors
DATABASE_URL must use the internal hostname spomeniqr-db:5432, not
localhost, and the app + database must be on the shared Coolify network.
12.6 Super-admin login fails
- 401:
SUPER_ADMIN_PASSWORD_HASHpasted with$$escaping → re-paste the raw$2b$12$…hash and redeploy. - 500 /
ADMIN_SESSION_SECRET env var is not set→ setADMIN_SESSION_SECRET(runtime) and restart. - Check boot log for
[seed] Super-admin 'super' provisioned.
12.7 CSP blocks Clerk JS (auth buttons dead)
Set NEXT_PUBLIC_CLERK_FAPI_HOST (e.g. clerk.testbed.mk) as a build
variable and redeploy — the CSP is generated at build time.
13. Maintenance & commands
| Action | Command / location |
|---|---|
| Redeploy app | Project → Application → Deploy |
| Re-apply proxy v3.6 pin | sed -i 's@traefik:v3.1@traefik:v3.6@' /data/coolify/proxy/docker-compose.yml && docker compose -f /data/coolify/proxy/docker-compose.yml up -d --force-recreate traefik |
| Restart proxy | docker restart coolify-proxy |
| Proxy logs | docker logs coolify-proxy -f |
| App logs | Project → Application → Logs / docker logs <app-container> -f |
| Force cert renewal | ~/.acme.sh/acme.sh --renew -d testbed.mk -d '*.testbed.mk' --force |
| Check cert status | ~/.acme.sh/acme.sh --list |
| Migrations | auto on container start; manual: docker exec <app> npx prisma migrate deploy |
| DB backup | Coolify UI database backup, or pg_dump against spomeniqr-db |
| Update app | push to Git; Coolify redeploys (or click Deploy) |
Environment variables quick reference
| Variable | Req | Notes |
|---|---|---|
DATABASE_URL |
Yes | Coolify-internal, host spomeniqr-db |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Yes | build var; pk_live_… |
NEXT_PUBLIC_CLERK_FAPI_HOST |
No* | build var; custom Clerk domain |
CLERK_SECRET_KEY |
Yes | runtime; sk_live_… |
NEXT_PUBLIC_CLERK_* paths |
Yes | build vars (see §7) |
S3_ENDPOINT / S3_REGION |
Yes | Contabo object storage |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY |
Yes | Contabo API keys |
S3_BUCKET_NAME |
Yes | monuments-images |
NEXT_PUBLIC_APP_URL |
Yes | build var; https://testbed.mk |
NEXT_PUBLIC_APP_DOMAIN |
Yes | build var; testbed.mk |
ADMIN_SESSION_SECRET |
Yes | runtime; openssl rand -hex 32 |
SUPER_ADMIN_USERNAME / SUPER_ADMIN_PASSWORD_HASH |
No | bcrypt hash, literal |
NODE_ENV |
Yes | production |
* only when using a custom Clerk frontend API domain (clerk.testbed.mk).