Compare commits
No commits in common. "main" and "admin" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,7 +9,6 @@
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next_old/
|
||||
/out/
|
||||
|
||||
# production
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@ -9,17 +9,6 @@ RUN npm ci
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache openssl
|
||||
# NEXT_PUBLIC_* vars are inlined into the client bundle at build time. Pass
|
||||
# them via build args (Coolify: mark them as build variables) or the browser
|
||||
# ClerkProvider / public URLs will be missing.
|
||||
ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
|
||||
ARG NEXT_PUBLIC_CLERK_FAPI_HOST
|
||||
ARG NEXT_PUBLIC_CLERK_SIGN_IN_URL
|
||||
ARG NEXT_PUBLIC_CLERK_SIGN_UP_URL
|
||||
ARG NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL
|
||||
ARG NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL
|
||||
ARG NEXT_PUBLIC_APP_URL
|
||||
ARG NEXT_PUBLIC_APP_DOMAIN
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY package.json package-lock.json ./
|
||||
COPY next.config.ts ./
|
||||
|
||||
@ -10,26 +10,13 @@ services:
|
||||
- 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.routers.spomeniqr-wildcard.rule=HostRegexp(`^[a-z0-9-]+\.testbed\.mk$$`)
|
||||
- traefik.http.routers.spomeniqr-wildcard.entryPoints=http,https
|
||||
- traefik.http.routers.spomeniqr-wildcard.service=spomeniqr-svc
|
||||
- traefik.http.routers.spomeniqr-wildcard.tls=true
|
||||
- traefik.http.routers.spomeniqr-wildcard.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.spomeniqr-svc.loadbalancer.server.port=3000
|
||||
- traefik.http.middlewares.gzip.compress=true
|
||||
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
|
||||
- traefik.docker.network=coolify
|
||||
|
||||
networks:
|
||||
coolify:
|
||||
|
||||
104
docs/coolify.md
104
docs/coolify.md
@ -1,11 +1,5 @@
|
||||
# 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
|
||||
@ -66,26 +60,9 @@ Internet
|
||||
- **Name:** `spomeniqr`
|
||||
- **Repository URL:** your Git repo URL
|
||||
- **Branch:** `main`
|
||||
- **Build Pack:** **Docker** (recommended) or **Nixpacks**
|
||||
- **Build Pack:** **Nixpacks** (auto-detected) or **Docker** (uses the Dockerfile)
|
||||
|
||||
### 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)
|
||||
### Option A: Nixpacks (Recommended — simpler)
|
||||
|
||||
Leave the build pack as **Nixpacks**. Coolify will auto-detect Next.js and build it.
|
||||
|
||||
@ -99,9 +76,11 @@ 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).
|
||||
### Option B: Docker (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 migration and standalone server startup.
|
||||
|
||||
**Note:** If using Docker build pack, the `DATABASE_URL` must use `spomeniqr-db` as the host (Coolify internal network), not `localhost`.
|
||||
|
||||
## Step 4: Configure Environment Variables
|
||||
|
||||
@ -114,9 +93,6 @@ 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=/
|
||||
@ -150,30 +126,10 @@ NODE_ENV=production
|
||||
- `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
|
||||
use a custom Nixpacks start command (Option A 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
|
||||
|
||||
@ -240,14 +196,6 @@ traefik.http.middlewares.spomeniqr-subdomain.headers.customrequestheaders.X-Subd
|
||||
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
|
||||
|
||||
@ -294,12 +242,8 @@ traefik.http.middlewares.spomeniqr-subdomain.headers.customrequestheaders.X-Subd
|
||||
- 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
|
||||
3. Once deployed, visit `https://testbed.mk` to verify
|
||||
|
||||
## Step 9: Verify Subdomain Routing
|
||||
|
||||
@ -321,13 +265,6 @@ If subdomains aren't working:
|
||||
# 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
|
||||
|
||||
@ -360,22 +297,6 @@ Make sure you've applied the bucket policy for public read. See Step 7. If Conta
|
||||
- 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
|
||||
|
||||
@ -398,12 +319,9 @@ npx prisma db push
|
||||
|----------|----------|-------------|
|
||||
| `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 |
|
||||
@ -416,10 +334,6 @@ npx prisma db push
|
||||
| `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
|
||||
|
||||
@ -1,652 +0,0 @@
|
||||
# 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](https://coolify.io/). 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
|
||||
|
||||
1. [Working stack snapshot](#1-working-stack-snapshot)
|
||||
2. [Prerequisites](#2-prerequisites)
|
||||
3. [DNS configuration](#3-dns-configuration)
|
||||
4. [Install Coolify](#4-install-coolify)
|
||||
5. [Create the project & database](#5-create-the-project--database)
|
||||
6. [Add the application](#6-add-the-application)
|
||||
7. [Environment variables](#7-environment-variables)
|
||||
8. [Domain & subdomain routing (Traefik labels)](#8-domain--subdomain-routing-traefik-labels)
|
||||
9. [Subdomain certificates (wildcard TLS)](#9-subdomain-certificates-wildcard-tls)
|
||||
10. [Deploy the application](#10-deploy-the-application)
|
||||
11. [Verify everything](#11-verify-everything)
|
||||
12. [Troubleshooting](#12-troubleshooting)
|
||||
13. [Maintenance & commands](#13-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](#121-traefik-cannot-talk-to-docker-29) — 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.1` still works, but the
|
||||
production setup documented here assumes Docker 29 and pins Traefik `v3.6`.
|
||||
- Domain `testbed.mk` registered, 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 (see
|
||||
`docs/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 |
|
||||
|
||||
- `@` makes `testbed.mk` resolve → VPS.
|
||||
- `*` (wildcard) makes **every** `*.testbed.mk` resolve → VPS. Required for
|
||||
tenant subdomains and for the wildcard cert's DNS-01 **ACME validation
|
||||
domain** (`_acme-challenge.testbed.mk`).
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
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.mk`
|
||||
> **TXT** record which the DNS client creates automatically via API. The
|
||||
> Contabo API credentials in `dns_contabo.sh` must therefore have permission to
|
||||
> manage `testbed.mk` records.
|
||||
|
||||
---
|
||||
|
||||
## 4. Install Coolify
|
||||
|
||||
Use the official installer (`coolify.io/docs/installation`) or the Vagrant-free
|
||||
server method. On a fresh VPS:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
1. Dashboard → **+ Add New Project** → name it `SpomeniQR`.
|
||||
2. Inside the project → **+ Add New Resource → Database → PostgreSQL**:
|
||||
- Name: `spomeniqr-db`
|
||||
- PostgreSQL version: **16**
|
||||
- Database name: `monuments`
|
||||
- Username: `postgres`
|
||||
- Password: strong, unique
|
||||
3. **Deploy**. Copy the **Internal Connection String**, e.g.:
|
||||
`postgresql://postgres:PASSWORD@spomeniqr-db:5432/monuments`
|
||||
(the host is the Coolify-internal service name `spomeniqr-db`, not `localhost`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Add the application
|
||||
|
||||
1. Inside the project → **+ Add New Resource → Application**.
|
||||
2. 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).
|
||||
3. Wire the database to the app on the shared Coolify network (`quick connect`
|
||||
button or ensure both are on `coolify` network).
|
||||
|
||||
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**):
|
||||
|
||||
```env
|
||||
# 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 at `next build`. Changing one requires a
|
||||
**redeploy** (not just a restart). `NEXT_PUBLIC_CLERK_FAPI_HOST` in particular
|
||||
must be a build var or the build-time CSP blocks Clerk JS.
|
||||
- `SUPER_ADMIN_PASSWORD_HASH` is passed **literally** by Coolify — paste the raw
|
||||
`$2b$12$…` hash with no `$$` escaping (that escaping is only for `env_file`
|
||||
Compose 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_SECRET` missing ⇒ 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:
|
||||
|
||||
```yaml
|
||||
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`, …) → same `app:3000`. The app's middleware then
|
||||
maps the subdomain to a memorial page.
|
||||
- Both are `tls=true` **without a certresolver**, so Traefik uses the
|
||||
**default certificate** — which is the wildcard cert from
|
||||
[§9 Subdomain certificates](#9-subdomain-certificates-wildcard-tls). No
|
||||
`tls.certresolver` label is needed on these routers.
|
||||
|
||||
> Because Coolify's own default routers can also be generated from the UI
|
||||
> `Domains` panel, prefer adding `testbed.mk` / `*.testbed.mk` there **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`
|
||||
|
||||
1. Install `acme.sh` (non-root or root):
|
||||
```bash
|
||||
curl https://get.acme.sh | sh -s email=you@example.com
|
||||
```
|
||||
2. Place `dns_contabo.sh` where acme.sh finds API plugins. It is written to be
|
||||
self-contained (uses `acme.sh` built-ins `_post`/`_info`/`_err`), so the
|
||||
cleanest approach is:
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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}_ecc` directory must contain `testbed.mk.cer`
|
||||
> and 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](#122-proxy-mount-layout)).
|
||||
Two things must be provisioned there:
|
||||
|
||||
**a) Copy the cert files into Coolify's certs directory:**
|
||||
|
||||
```bash
|
||||
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/…`):
|
||||
|
||||
```yaml
|
||||
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):
|
||||
|
||||
```bash
|
||||
~/.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-file` tell acme.sh to **copy** the renewed certs
|
||||
into Coolify's certs directory on every renewal.
|
||||
- `--reloadcmd` restarts 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
|
||||
|
||||
```bash
|
||||
# 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 `letsencrypt` certresolver
|
||||
> 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
|
||||
> from `HostRegexp` routers. The manual DNS-01 wildcard + default-cert approach
|
||||
> is the correct one for this app.
|
||||
|
||||
---
|
||||
|
||||
## 10. Deploy the application
|
||||
|
||||
1. Ensure `[§7 env vars]` are saved and all `NEXT_PUBLIC_*` are marked as build
|
||||
variables **before** the first build.
|
||||
2. 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**.
|
||||
3. If the routing (Traefik) labels changed (e.g. §8 edits in the repo), triggers
|
||||
a **recreate** so the new labels apply.
|
||||
|
||||
---
|
||||
|
||||
## 11. Verify everything
|
||||
|
||||
```bash
|
||||
# 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`, then `https://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`):
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
# /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:
|
||||
|
||||
```bash
|
||||
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 `HostRegexp` label.** Ensure both
|
||||
`testbed-wildcard` and `testbed-wildcard-http` use the v3 form
|
||||
`HostRegexp(\`^[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 +short` must return the VPS IP.
|
||||
- **Duplicate routers**: if you also added `*.testbed.mk` in Coolify's UI
|
||||
`Domains`, 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' --force`
|
||||
and 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_HASH` pasted with `$$` escaping → re-paste the raw
|
||||
`$2b$12$…` hash and redeploy.
|
||||
- 500 / `ADMIN_SESSION_SECRET env var is not set` → set `ADMIN_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`).
|
||||
@ -1,19 +1,13 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
|
||||
@ -4,12 +4,9 @@ const s3Host = process.env.S3_ENDPOINT
|
||||
? new URL(process.env.S3_ENDPOINT).hostname
|
||||
: "";
|
||||
|
||||
// Derive the Clerk frontend API host so the CSP allowlist always matches the
|
||||
// active environment. When a custom Clerk frontend API domain is configured in
|
||||
// the Clerk dashboard (e.g. clerk.testbed.mk), it is resolved at runtime by
|
||||
// clerk-js and can NOT be derived from the publishable key — so it must be
|
||||
// provided explicitly via NEXT_PUBLIC_CLERK_FAPI_HOST. Otherwise Clerk
|
||||
// supports two publishable key formats:
|
||||
// Derive the Clerk frontend API host from the publishable key so the
|
||||
// CSP allowlist always matches the active environment. Clerk supports
|
||||
// two publishable key formats:
|
||||
//
|
||||
// 1) "Encoded" form (older): pk_test_<base64slug>$
|
||||
// The base64 portion decodes to "<slug>.clerk.accounts.dev" (test)
|
||||
@ -20,16 +17,6 @@ const s3Host = process.env.S3_ENDPOINT
|
||||
// The slug may itself contain hyphens and digits, so only the
|
||||
// final dash-group is captured as the suffix.
|
||||
function clerkFrontendApiHost(): string | null {
|
||||
const custom = process.env.NEXT_PUBLIC_CLERK_FAPI_HOST?.trim();
|
||||
if (custom) {
|
||||
try {
|
||||
const host = new URL(custom.includes("://") ? custom : `https://${custom}`).hostname;
|
||||
if (host) return host;
|
||||
} catch {
|
||||
// fall through to derivation from the publishable key
|
||||
}
|
||||
}
|
||||
|
||||
const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
||||
if (!key) return null;
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { PrismaClient, Role } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { S3Client, PutBucketPolicyCommand } = require("@aws-sdk/client-s3");
|
||||
|
||||
const client = new S3Client({
|
||||
|
||||
@ -7,7 +7,6 @@ echo "=== SpomeniQR Starting ==="
|
||||
echo "DATABASE_URL: ${DATABASE_URL:+set}"
|
||||
echo "CLERK_SECRET_KEY: ${CLERK_SECRET_KEY:+set}"
|
||||
echo "S3_ENDPOINT: ${S3_ENDPOINT:+set}"
|
||||
echo "ADMIN_SESSION_SECRET: ${ADMIN_SESSION_SECRET:+set}"
|
||||
|
||||
echo "Running Prisma migrations..."
|
||||
if ! npx prisma migrate deploy; then
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "СпоменQR — Во спомен на",
|
||||
@ -11,12 +10,12 @@ export default function NotFound() {
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||
<h1 className="text-4xl font-bold text-stone-900">Спомен страницата не е пронајдена</h1>
|
||||
<p className="mt-4 text-stone-600">Оваа спомен страница не постои или сè уште не е објавена.</p>
|
||||
<Link
|
||||
<a
|
||||
href="/"
|
||||
className="mt-8 rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
||||
>
|
||||
Почетна
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user