# Deploying SpomeniQR on Coolify > **Canonical reference:** this guide is maintained for background/context. The > **verified production walkthrough** (working Traefik setup, v3 `HostRegexp` > routing labels, Docker 29 compatibility, and a dedicated **wildcard subdomain > certificate** section) lives in **[coolifyDeployFinal.md](./coolifyDeployFinal.md)**. > Start there for a new deployment; keep this file's notes below in mind as well. This guide covers deploying SpomeniQR to a VPS using [Coolify](https://coolify.io/) — 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](https://coolify.io/docs/installation)) - 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 Resource** → **Database** 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 Resource** → **Application** 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** ### Option A: Docker (Recommended — uses the project's Dockerfile) 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 `ARG`s, 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: ```env # 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 **Configuration** → **Domains** 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 #### Option A: Coolify Proxy Configuration (Recommended) 1. In your application, go to **Configuration** → **Domains** 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: ```yaml 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](https://dashboard.clerk.com) 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: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": { "AWS": ["*"] }, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::monuments-images/*"] } ] } ``` 4. Set **CORS** to allow uploads: ```json [ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET", "PUT", "POST"], "AllowedOrigins": ["https://testbed.mk", "https://*.testbed.mk"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3600 } ] ``` 5. 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 ```bash # 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: ```bash # 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