spomeni/docs/coolify.md
2026-08-03 19:43:28 +02:00

14 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 A below), run the seed manually after the first deploy: npx prisma db seed.

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

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
    • Start the server on port 3000
  3. 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

# 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

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
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

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