11 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.mkwith DNS configured:- A record
@→ your VPS IP - A record
*→ your VPS IP (wildcard for subdomains)
- A record
- A Contabo S3 bucket set up with public read policy
Architecture Overview
Coolify will run two containers:
- app — Next.js standalone build (Dockerfile)
- 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
- Open your Coolify dashboard
- Click + Add New Project
- Name it
SpomeniQR
Step 2: Create the Database
- Inside the project, click + Add New Resource → Database
- Select PostgreSQL
- Configure:
- Name:
spomeniqr-db - PostgreSQL Version: 16
- Database Name:
monuments - Username:
postgres - Password: generate a strong password or set your own
- Name:
- Click Deploy
- After deployment, note the Internal Connection String — it looks like:
You'll need this forpostgresql://postgres:YOUR_PASSWORD@spomeniqr-db:5432/monumentsDATABASE_URL.
Step 3: Add the Application
- Inside the project, click + Add New Resource → Application
- Select Public Repository (or Private if your repo is private)
- Configure:
- Name:
spomeniqr - Repository URL: your Git repo URL
- Branch:
main - Build Pack: Nixpacks (auto-detected) or Docker (uses the Dockerfile)
- Name:
Option A: Nixpacks (Recommended — simpler)
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
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
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_...
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
# Node
NODE_ENV=production
Important:
DATABASE_URLmust point to the Coolify internal hostname (spomeniqr-db), notlocalhost.- Use your production Clerk keys (
pk_live_/sk_live_), not the test ones.
Step 5: Configure Domain & Subdomain Routing
Set the Main Domain
- In the application settings, go to Configuration → Domains
- Add:
testbed.mk - 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:
- Accept requests for
*.testbed.mk - Extract the subdomain and pass it as the
X-Subdomainheader to the Next.js app
Option A: Coolify Proxy Configuration (Recommended)
- In your application, go to Configuration → Domains
- Add both domains:
testbed.mk*.testbed.mk
- 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
- Go to Clerk Dashboard
- Switch to your Production instance
- Under Paths, set:
- Sign-in:
/sign-in - Sign-up:
/sign-up
- Sign-in:
- Under Domains, add:
testbed.mk*.testbed.mk
Step 7: Configure Contabo S3
- Log into Contabo Object Storage
- Create a bucket named
monuments-imagesin regioneu-2 - 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/*"]
}
]
}
- Set CORS to allow uploads:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedOrigins": ["https://testbed.mk", "https://*.testbed.mk"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
- Create an API key with read/write permissions
Step 8: Deploy
- Click Deploy in the Coolify dashboard
- 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
- Once deployed, visit
https://testbed.mkto verify
Step 9: Verify Subdomain Routing
Test that wildcard subdomains work:
- Create a memorial page with subdomain
test-memorial - Visit
https://test-memorial.testbed.mk - Check browser DevTools network tab — the
X-Subdomainheader should be set by the proxy - The Next.js middleware reads
X-Subdomainand rewrites to the correct page
If subdomains aren't working:
- Check DNS:
dig *.testbed.mk +shortshould return your VPS IP - Check Coolify proxy logs for the wildcard domain configuration
- Verify the
X-Subdomainheader 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.mkpoints to your VPS - Check that Coolify's proxy config includes both
testbed.mkand*.testbed.mk - Check the proxy access logs — the
X-Subdomainheader 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.mkand*.testbed.mkare added in Clerk dashboard domains - Check that
NEXT_PUBLIC_APP_URLandNEXT_PUBLIC_APP_DOMAINare 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_...) |
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 |
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:
- Push changes to your Git repository
- Coolify will auto-deploy if Auto Deploy is enabled, or click Deploy manually
- The
start.shscript runsnpx prisma migrate deployon every startup, so schema changes are applied automatically