Coolify Setup Guide
Everything to finish the move off Vercel: assign real domains, put Cloudflare in front for CDN + firewall, lock down the server, get analytics & speed insights into your own dashboards, and run hourly backups of every SaaS, client and user. Tick items as you go — progress saves in this browser.
i Overview & infrastructure map
Two DigitalOcean servers, one Coolify control panel.
159.65.235.140Coolify panel (
:8000 → api.boommedia.us) + self-hosted Supabase (db.boommedia.us). 4 GB / 2 vCPU.
24.199.95.101Where the Vercel apps now run. Nixpacks builds from GitHub. Auto URLs on
*.sslip.io.
*.sslip.io into real domains, wrap them in Cloudflare, and stand up monitoring + backups so this stack matches (and in places beats) what Vercel gave you.1 Assign real domains + SSL
8 of 9 apps still show a random *.24.199.95.101.sslip.io URL. Only untouchables.life has a real domain. Do this per app.
Per-app steps
- In your DNS provider, add an A record:
app-domain.com → 24.199.95.101.
If the domain is on Cloudflare (Section 2), set it DNS-only / grey cloud for now so the cert can issue. - Coolify → the app → Domains → enter
https://app-domain.com. - Coolify + Traefik auto-request a Let's Encrypt certificate. Wait ~30s.
- Turn on Force HTTPS redirect.
- Reload over
https://and confirm the padlock + the app loads.
Track it
| App | Planned domain | DNS A record | SSL live |
|---|---|---|---|
| untouchables | untouchables.life | done | done |
| bm-portal | ___________ | todo | todo |
| bmbb-vercel | ___________ | todo | todo |
| eric-doerr | ___________ | todo | todo |
| haw-portal | ___________ | todo | todo |
| haw-vercel | ___________ | todo | todo |
| rar-vercel | ___________ | todo | todo |
| rlg-vercel | ___________ | todo | todo |
| posttee (SaaS) | posttee.online | todo | todo |
- All 8 apps have an A record → 24.199.95.101
- All 8 have a real domain entered in Coolify with Let's Encrypt SSL live
- Force-HTTPS on for every app
- Updated OAuth redirect URIs / Stripe webhook URLs to the new domainsAnything that referenced the old Vercel or sslip.io URL
2 Cloudflare — CDN, WAF & free analytics
This one free account replaces most of the Vercel sidebar at once: CDN, Firewall/WAF, DDoS, Web Analytics and Core Web Vitals. Do this first — biggest win for least effort.
Set up
- Create a free Cloudflare account → Add site → your domain.
- Cloudflare scans existing DNS → change your nameservers at the registrar to the two Cloudflare gives you.
- For each app's record: point to
24.199.95.101, then enable the orange cloud (Proxied) — after SSL has issued (Section 1). - SSL/TLS mode → set to Full (strict) (works because Traefik has a valid Let's Encrypt cert).
Turn on the free parity features
| Vercel feature | Cloudflare setting | |
|---|---|---|
| Edge CDN | Proxied DNS (orange cloud) + Speed → Optimization → Brotli, Auto Minify | free |
| Firewall / WAF | Security → WAF → enable Managed Ruleset; add rate-limiting rule | free |
| DDoS protection | On by default when proxied; Under Attack Mode toggle for emergencies | free |
| Web Analytics | Analytics → Web Analytics → enable for the domain (no snippet needed when proxied) | free |
| Speed Insights (Web Vitals) | Same Web Analytics panel reports LCP / CLS / INP real-user metrics | free |
/_next/static, so it's safe to cache forever. Add a Cloudflare Cache Rule or set headers in next.config.js:
// next.config.js — long-cache immutable assets
async headers() {
return [{
source: '/_next/static/:path*',
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
}];
}
- Domains added to Cloudflare, nameservers switched, status Active
- Records proxied (orange cloud), SSL/TLS = Full (strict)
- WAF Managed Ruleset enabled + a rate-limit rule
- Web Analytics enabled (gives analytics + Core Web Vitals)
- Brotli + Auto Minify on; static-asset cache rule live
3 Firewall lockdown (the server itself)
Cloudflare guards the edge; now close the droplet so nobody bypasses it. Two layers.
3a · DigitalOcean Cloud Firewall (do this first — it's outside the box)
DO dashboard → Networking → Firewalls → Create. Attach to both droplets. Inbound rules:
| Port | Source | Why |
|---|---|---|
| 22 (SSH) | Your IP only | admin access |
| 80 / 443 | All IPv4/IPv6 | web traffic (ideally Cloudflare IP ranges only) |
| 8000 | Your IP only | Coolify dashboard |
3b · UFW on each droplet (defense in depth)
SSH into each server and run:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow from YOUR.IP.HERE to any port 8000 # Coolify panel, restricted
ufw enable
ufw status verbose
ufw enable. Keep the DigitalOcean web console open as a fallback in case the rule is wrong.- DO Cloud Firewall created + attached to both droplets
- SSH (22) + Coolify (8000) restricted to your IP
- UFW enabled on both droplets, status verified
- (Optional) 80/443 restricted to Cloudflare IP ranges
4 Analytics & Speed Insights — in your own dashboards
Coolify shows container logs + CPU/RAM/disk, but not web analytics or web vitals. You said you want to "see all that stuff" — here's how to get real dashboards. Cloudflare (Section 2) already covers the basics for free; deploy these when you want data you own, inside Coolify.
4a · Umami — self-hosted web analytics + events free
Privacy-first, GDPR-friendly, own your data. Deploy as its own Coolify app:
- Coolify → + New → Application → Docker Image →
ghcr.io/umami-software/umami:postgresql-latest. - Give it a Postgres DB (add a small Coolify-managed Postgres, or a new DB on the Supabase box).
- Set env:
DATABASE_URL,APP_SECRET(any random string). - Assign a domain e.g.
analytics.boommedia.us→ SSL auto-issues. - Add the Umami
<script>snippet to each SaaS app's layout. One Umami instance tracks all apps — filter by website.
4b · Core Web Vitals into Umami / your DB
Ship real-user LCP/CLS/INP from each Next.js app:
// app/_components/WebVitals.tsx (Next App Router)
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((m) => {
// send to your own endpoint, Umami event, or Supabase table
navigator.sendBeacon('/api/vitals', JSON.stringify(m));
});
}
Render <WebVitals/> in the root layout. Cloudflare Web Analytics also shows these with zero code — use that first, add this when you want per-route detail.
4c · Infra dashboards beyond Coolify (optional, advanced)
For CPU/RAM/network graphs and alerting across all apps in one pane, deploy the classic stack as Coolify apps: Grafana + Prometheus + cAdvisor + node-exporter. Overkill until you run 5+ SaaS apps here — Coolify's built-in per-app metrics are enough for now.
4d · Uptime + error tracking (do these — they're the real gaps vs Vercel)
One-click in Coolify. Pings every domain, alerts (email/Slack) on downtime. Cheap, high value.
Add to each Next.js app for exceptions + performance traces. Biggest gap vs Vercel — do early.
- Cloudflare Web Analytics live (quick win — analytics + web vitals)
- Umami deployed in Coolify + snippet in each appanalytics.boommedia.us
- Uptime Kuma monitoring every domain with alerts
- Sentry added to each app for errors + performance
- (Optional) Grafana + Prometheus stack for infra graphs
5 Hourly backups — every SaaS, client & user
What gets backed up
| Layer | Method | Frequency |
|---|---|---|
| Supabase Postgres (all data) | pg_dump → Backblaze B2 offsite | Hourly |
| Uploaded files / Storage | rclone sync volume → B2 | Daily |
| Whole droplet | DigitalOcean snapshot | Weekly |
| App code | already in GitHub | on push |
5a · Hourly Postgres backup
If Postgres is a Coolify-managed DB resource: Coolify → the DB → Backups → schedule cron 0 * * * *, destination = Backblaze B2 (S3), set retention. Done.
If Supabase is a Docker-compose Service (the one-click deploy), use a host cron script:
# /opt/boom-backups/pg_backup.sh
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%Y%m%d-%H%M)
OUT=/opt/boom-backups/pg; mkdir -p "$OUT"
DB_CONTAINER=supabase-db # find with: docker ps | grep supabase-db
docker exec "$DB_CONTAINER" pg_dumpall -U postgres | gzip > "$OUT/all-$TS.sql.gz"
rclone copy "$OUT/all-$TS.sql.gz" b2:boom-backups/pg/ # offsite copy
ls -1t "$OUT"/all-*.sql.gz | tail -n +49 | xargs -r rm # keep 48h local
chmod +x /opt/boom-backups/pg_backup.sh
crontab -e
# add this line:
0 * * * * /opt/boom-backups/pg_backup.sh >> /var/log/boom-backup.log 2>&1
5b · Offsite target & retention
Use Backblaze B2 (~$6/TB/mo, cheapest) or DigitalOcean Spaces ($5/mo). Set once with rclone config (remote name b2). Retention via B2 lifecycle rules:
- Hourly dumps — keep 48 hours
- One daily promoted — keep 14 days
- One weekly — keep 8 weeks
5c · Files, droplet & restore
# daily: uploaded files / Supabase Storage volume
0 3 * * * rclone sync /path/to/supabase/storage/volume b2:boom-backups/storage/
Whole droplet: DigitalOcean → Droplet → Backups (weekly automated) — full fallback if the box dies.
Restore one client/user: restore the dump into a scratch Postgres, then copy that tenant's rows (by org/user id) into the live DB. There's no per-tenant physical backup — the whole-DB dump is the source of truth.
- Backblaze B2 (or Spaces) bucket + rclone configured
- Hourly pg_dump running (Coolify schedule or cron), verified in B2
- B2 lifecycle retention (48h / 14d / 8w) set
- Daily Storage/volume sync running
- Weekly DigitalOcean droplet snapshots enabled
- Did a test restore & confirmed row counts
✓ Final checklist
The stack is at Vercel parity (and past it) when all of these are true.
- All 9 apps on real domains with HTTPS
- Cloudflare in front — CDN, WAF, analytics, web vitals live
- Server firewalls closed (DO Cloud Firewall + UFW)
- Analytics + uptime + error tracking dashboards up
- Hourly DB backups offsite + tested restore
- Old Vercel projects paused (delete after ~2 stable weeks)