spomeniV2/docs/coolify.md
Dimitar765 d6b29b8e9c
Some checks failed
CI / build (push) Has been cancelled
v2 initial commit
2026-08-03 20:33:49 +02:00

16 KiB

Deploying SpomeniQR on Coolify

This guide covers deploying SpomeniQR to a VPS using Coolify — a self-hosting platform similar to Heroku/Vercel that manages Docker deployments, SSL, and databases.

Prerequisites

  • A VPS with Docker installed (Coolify handles this)
  • Coolify installed on your VPS (see coolify.io/docs)
  • Domain testbed.mk with DNS configured:
    • A record @ → your VPS IP
    • A record * → your VPS IP (wildcard for subdomains)
  • A Contabo S3 bucket set up with public read policy

Architecture Overview

Coolify will run two containers:

  1. app — Next.js standalone build (Dockerfile)
  2. db — PostgreSQL 16 (Coolify managed database)

We do not deploy Nginx via Docker — Coolify has its own reverse proxy (Traefik/Caddy) that handles SSL, subdomain routing, and the X-Subdomain header.

Internet
  │
  ├── *.testbed.mk ──► Coolify Proxy (:80/:443)
  │                     ├── Extracts subdomain → sets X-Subdomain header
  │                     └── Proxies to app:3000
  │
  └── testbed.mk ──► Coolify Proxy ──► app:3000

Step 1: Add a New Project in Coolify

  1. Open your Coolify dashboard
  2. Click + Add New Project
  3. Name it SpomeniQR

Step 2: Create the Database

  1. Inside the project, click + Add New ResourceDatabase
  2. Select PostgreSQL
  3. Configure:
    • Name: spomeniqr-db
    • PostgreSQL Version: 16
    • Database Name: monuments
    • Username: postgres
    • Password: generate a strong password or set your own
  4. Click Deploy
  5. After deployment, note the Internal Connection String — it looks like:
    postgresql://postgres:YOUR_PASSWORD@spomeniqr-db:5432/monuments
    
    You'll need this for DATABASE_URL.

Step 3: Add the Application

  1. Inside the project, click + Add New ResourceApplication
  2. Select Public Repository (or Private if your repo is private)
  3. Configure:
    • Name: spomeniqr
    • Repository URL: your Git repo URL
    • Branch: main
    • Build Pack: Docker (recommended) or Nixpacks

Set Build Pack to Docker. Coolify will use the Dockerfile in the repo root. No additional configuration needed — it already includes Prisma generate, next build, migrations (prisma migrate deploy), and automatic super-admin provisioning on startup. This is the verified path (tested end-to-end locally).

Note: If using the Docker build pack, the DATABASE_URL must use spomeniqr-db as the host (Coolify internal network), not localhost.

Build-time variables: the NEXT_PUBLIC_* env vars are inlined into the browser bundle during next build. The Dockerfile declares matching ARGs, so mark every NEXT_PUBLIC_* variable as a build variable in Coolify (the checkbox on each env var). Without this, client-side Clerk auth (sign-in / sign-up) will have no publishable key. Server-side middleware also reads them at runtime, so keep them set as regular runtime vars too.

Option B: Nixpacks (auto-detected)

Leave the build pack as Nixpacks. Coolify will auto-detect Next.js and build it.

Add these Build Commands:

npx prisma generate && npm run build

Add this Start Command:

npx prisma migrate deploy && node .next/standalone/server.js

Note: With a custom start command the auto-seed does not run; provision the super-admin manually after the first deploy with npx prisma db seed (or see the seed note in Step 4).

Step 4: Configure Environment Variables

In the application settings, go to Environment Variables and add:

# Database — use the Coolify internal connection string
DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@spomeniqr-db:5432/monuments

# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
# Custom Clerk frontend API domain (if configured in Clerk dashboard) — the CSP
# allowlist needs it. Build variable. Omit if not using a custom domain.
NEXT_PUBLIC_CLERK_FAPI_HOST=clerk.testbed.mk
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 (64+ random hex chars; openssl rand -hex 32)
ADMIN_SESSION_SECRET=your-random-64-char-secret

# Super-admin — username + BCRYPT HASH (not plaintext). Generate with:
#   node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))"
# Coolify UI env vars are passed to the container literally — no `$` escaping needed.
SUPER_ADMIN_USERNAME=super
SUPER_ADMIN_PASSWORD_HASH=$2b$12$REPLACE_WITH_BCRYPT_HASH

# Node
NODE_ENV=production

Important:

  • DATABASE_URL must point to the Coolify internal hostname (spomeniqr-db), not localhost.
  • Use your production Clerk keys (pk_live_ / sk_live_), not the test ones.
  • Use a different, strong super-admin password than your local development one.
  • Generate the hash on your dev machine (in the project, so it uses the project's bcryptjs) and paste it verbatim — Coolify passes values literally, no $$ escaping:
    node -e "import('bcryptjs').then(b => b.default.hash('YOUR_PASSWORD', 12).then(console.log))"
    
  • Mark every NEXT_PUBLIC_* variable as a build variable (checkbox) so it is inlined into the client bundle at next build and visible to next.config.ts. NEXT_PUBLIC_CLERK_FAPI_HOST in particular must be a build variable — the CSP is generated at build time and will otherwise block Clerk JS if you use a custom Clerk frontend API domain. Server-only vars (CLERK_SECRET_KEY, DATABASE_URL, ADMIN_SESSION_SECRET, SUPER_ADMIN_PASSWORD_HASH, S3_*) stay as normal runtime variables.
  • The super admin row is provisioned automatically on container start by scripts/start.sh (node prisma/seed.cjs after migrations). If you instead use a custom Nixpacks start command (Option B below), run the seed manually after the first deploy: npx prisma db seed.
  • ADMIN_SESSION_SECRET is required at runtime — without it every super-admin login returns 500 (ADMIN_SESSION_SECRET env var is not set). Generate with openssl rand -hex 32. scripts/start.sh logs ADMIN_SESSION_SECRET: set at boot so a missing value is obvious.
  • Changing any NEXT_PUBLIC_* build variable requires a redeploy, not just a restart — those values are baked into the client bundle and the CSP at next build.

Step 5: Configure Domain & Subdomain Routing

Set the Main Domain

  1. In the application settings, go to ConfigurationDomains
  2. Add: testbed.mk
  3. Enable HTTPS — Coolify will auto-provision a Let's Encrypt certificate

Enable Wildcard Subdomain Routing

This is the critical part. Coolify's proxy needs to:

  1. Accept requests for *.testbed.mk
  2. Extract the subdomain and pass it as the X-Subdomain header to the Next.js app
  1. In your application, go to ConfigurationDomains
  2. Add both domains:
    • testbed.mk
    • *.testbed.mk
  3. Coolify will request a wildcard SSL certificate. If your DNS provider supports DNS-01 challenges (Cloudflare, Route53, etc.), this works automatically. Otherwise, you may need to add each subdomain manually.

Option B: Custom Proxy Configuration

If Coolify doesn't support wildcard domains easily, add a custom Caddy/Traefik configuration in the Coolify settings:

For Caddy (Coolify's default proxy):

Create a file at /data/coolify/proxy/caddy/custom/testbed.mk:

*.testbed.mk {
    reverse_proxy app:3000 {
        header_up X-Subdomain {http.request.host.labels.2}
        header_up X-Forwarded-Proto {scheme}
    }
}

testbed.mk {
    reverse_proxy app:3000 {
        header_up X-Forwarded-Proto {scheme}
    }
}

For Traefik (alternative proxy), you'd add labels to the container:

traefik.http.routers.spomeniqr.rule: HostRegexp(`{subdomain:[a-z0-9-]+}.testbed.mk`) || Host(`testbed.mk`)
traefik.http.middlewares.spomeniqr-subdomain.headers.customrequestheaders.X-Subdomain: 

Note: The proxy configuration varies based on your Coolify version and proxy choice. Check the Coolify docs for the latest instructions on wildcard domains.

Step 6: Configure Clerk

  1. Go to Clerk Dashboard
  2. Switch to your Production instance
  3. Under Paths, set:
    • Sign-in: /sign-in
    • Sign-up: /sign-up
  4. Under Domains, add:
    • testbed.mk
    • *.testbed.mk
  5. Custom frontend API domain (recommended): under Domains, set clerk.testbed.mk as the Frontend API custom domain and add the DNS record Clerk instructs (a CNAME). clerk-js then loads from https://clerk.testbed.mk/npm/@clerk/clerk-js@.... Because this domain can't be derived from the publishable key at build time, you must set NEXT_PUBLIC_CLERK_FAPI_HOST=clerk.testbed.mk (Step 4) as a build variable — otherwise the app's CSP blocks clerk.browser.js and the auth-gated buttons on the home page stop working.

Step 7: Configure Contabo S3

  1. Log into Contabo Object Storage
  2. Create a bucket named monuments-images in region eu-2
  3. Set the bucket policy for public read:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": { "AWS": ["*"] },
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::monuments-images/*"]
    }
  ]
}
  1. Set CORS to allow uploads:
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST"],
    "AllowedOrigins": ["https://testbed.mk", "https://*.testbed.mk"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
  1. Create an API key with read/write permissions

Step 8: Deploy

  1. Click Deploy in the Coolify dashboard
  2. Watch the build logs — it should:
    • Install dependencies
    • Run npx prisma generate
    • Build the Next.js app
    • Run npx prisma migrate deploy
    • Provision the super-admin ([seed] Super-admin 'super' provisioned.)
    • Start the server on port 3000
  3. First deploy: make sure every NEXT_PUBLIC_* variable (including NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and NEXT_PUBLIC_CLERK_FAPI_HOST) is already marked as a build variable — they're baked in during this build.
  4. Once deployed, visit https://testbed.mk to verify

Step 9: Verify Subdomain Routing

Test that wildcard subdomains work:

  1. Create a memorial page with subdomain test-memorial
  2. Visit https://test-memorial.testbed.mk
  3. Check browser DevTools network tab — the X-Subdomain header should be set by the proxy
  4. The Next.js middleware reads X-Subdomain and rewrites to the correct page

If subdomains aren't working:

  • Check DNS: dig *.testbed.mk +short should return your VPS IP
  • Check Coolify proxy logs for the wildcard domain configuration
  • Verify the X-Subdomain header is being set in the proxy config

Step 10: Verify Everything

# App health
curl -s https://testbed.mk | head -5

# CSP must allowlist the Clerk frontend API host (script-src + connect-src)
curl -sI https://testbed.mk | grep -i content-security-policy

# Super-admin login (expect {"success":true})
curl -s -X POST https://testbed.mk/api/admin/login -H "Content-Type: application/json" \
  -d '{"username":"super","password":"YOUR_PASSWORD"}'

# Subdomain routing
curl -sI https://test-memorial.testbed.mk | head -5

# API health
curl -s https://testbed.mk/api/check-subdomain?slug=test | python3 -m json.tool

# Database connection (from Coolify terminal)
docker exec spomeniqr-app npx prisma db push --accept-data-loss

Troubleshooting

Build fails with Prisma errors

Make sure DATABASE_URL points to the Coolify internal hostname (e.g., spomeniqr-db:5432), not localhost. The app and database must be on the same Coolify network.

Images return 401 from S3

Make sure you've applied the bucket policy for public read. See Step 7. If Contabo doesn't serve public objects via URL, the app uses an /api/image?key=... proxy route as a fallback.

Subdomain routing not working

  • Verify DNS wildcard *.testbed.mk points to your VPS
  • Check that Coolify's proxy config includes both testbed.mk and *.testbed.mk
  • Check the proxy access logs — the X-Subdomain header should appear
  • If using a custom Caddyfile, make sure it's in the right directory and reload the proxy

Clerk authentication issues

  • Ensure production Clerk keys are set (not pk_test_ / sk_test_)
  • Verify testbed.mk and *.testbed.mk are added in Clerk dashboard domains
  • Check that NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_APP_DOMAIN are set correctly
  • CSP blocking Clerk JS — if the browser console shows Content-Security-Policy: ... blocked a script ... clerk.testbed.mk/npm/@clerk/clerk-js... (and auth buttons do nothing): the CSP was built without the Clerk frontend API host. Set NEXT_PUBLIC_CLERK_FAPI_HOST (Step 4) as a build variable and redeploy — the CSP is generated at build time.

Super-admin login fails

  • 401 "Невалидно корисничко име или лозинка" — wrong password, or SUPER_ADMIN_PASSWORD_HASH was pasted with $$ escaping (the dev Compose format) so the stored hash is wrong. Re-paste the raw $2b$12$... hash and redeploy (the seed upserts the row on boot).
  • 500 / "ADMIN_SESSION_SECRET env var is not set"ADMIN_SESSION_SECRET is missing. Add it (runtime var) and restart.
  • Verify the boot log contains [seed] Super-admin 'super' provisioned. — the seed skips silently when SUPER_ADMIN_PASSWORD_HASH is unset.

Database migration issues

If migrations fail on deploy, you can run them manually from the Coolify terminal:

# SSH into the app container
docker exec -it spomeniqr-app sh

# Run migrations
npx prisma migrate deploy

# Or push schema changes directly
npx prisma db push

Environment Variables Reference

Variable Required Description
DATABASE_URL Yes PostgreSQL connection string (Coolify internal)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY Yes Clerk publishable key (pk_live_...)
NEXT_PUBLIC_CLERK_FAPI_HOST No* Custom Clerk frontend API domain (clerk.testbed.mk) — required when using a custom domain; build variable
CLERK_SECRET_KEY Yes Clerk secret key (sk_live_...)
NEXT_PUBLIC_CLERK_SIGN_IN_URL Yes /sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL Yes /sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL Yes Redirect after sign-in (/)
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL Yes Redirect after sign-up (/)
S3_ENDPOINT Yes Contabo S3 endpoint URL
S3_REGION Yes Contabo S3 region (e.g. eu-2)
S3_ACCESS_KEY_ID Yes S3 access key
S3_SECRET_ACCESS_KEY Yes S3 secret key
S3_BUCKET_NAME Yes S3 bucket name
NEXT_PUBLIC_APP_URL Yes https://testbed.mk
NEXT_PUBLIC_APP_DOMAIN Yes testbed.mk
ADMIN_SESSION_SECRET Yes Secret signing admin session cookies (openssl rand -hex 32)
SUPER_ADMIN_USERNAME No Super-admin username (default super)
SUPER_ADMIN_PASSWORD_HASH No Super-admin bcrypt hash; if unset, super login is unavailable
NODE_ENV Yes production

* NEXT_PUBLIC_CLERK_FAPI_HOST is only needed when a custom Clerk frontend API domain is configured in the Clerk dashboard. It must be a build variable so the CSP (generated at next build) allowlists clerk.browser.js.

Useful Coolify Commands

  • Redeploy: Project → Application → Deploy
  • View Logs: Project → Application → Logs
  • SSH into container: Project → Application → Terminal
  • Database Management: Project → Database → Admin (pgAdmin or Prisma Studio)
  • SSL Certificates: Managed automatically by Coolify for configured domains

Updates

To update the application:

  1. Push changes to your Git repository
  2. Coolify will auto-deploy if Auto Deploy is enabled, or click Deploy manually
  3. The start.sh script runs npx prisma migrate deploy on every startup, so schema changes are applied automatically