Self-hosted hosting stack

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.

Control + Supabase
159.65.235.140
Coolify panel (:8000 → api.boommedia.us) + self-hosted Supabase (db.boommedia.us). 4 GB / 2 vCPU.
App hosting — boom-hosting
24.199.95.101
Where the Vercel apps now run. Nixpacks builds from GitHub. Auto URLs on *.sslip.io.
The goal of this guide Turn the 8 apps still on *.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

  1. 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.
  2. Coolify → the app → Domains → enter https://app-domain.com.
  3. Coolify + Traefik auto-request a Let's Encrypt certificate. Wait ~30s.
  4. Turn on Force HTTPS redirect.
  5. Reload over https:// and confirm the padlock + the app loads.
Order matters with Cloudflare Issue the Let's Encrypt cert with the DNS record grey-clouded first, then switch to orange (proxied) in Section 2. Orange-cloud during first issuance can block validation.

Track it

AppPlanned domainDNS A recordSSL live
untouchablesuntouchables.lifedonedone
bm-portal___________todotodo
bmbb-vercel___________todotodo
eric-doerr___________todotodo
haw-portal___________todotodo
haw-vercel___________todotodo
rar-vercel___________todotodo
rlg-vercel___________todotodo
posttee (SaaS)posttee.onlinetodotodo
  • 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

  1. Create a free Cloudflare account → Add site → your domain.
  2. Cloudflare scans existing DNS → change your nameservers at the registrar to the two Cloudflare gives you.
  3. For each app's record: point to 24.199.95.101, then enable the orange cloud (Proxied)after SSL has issued (Section 1).
  4. SSL/TLS mode → set to Full (strict) (works because Traefik has a valid Let's Encrypt cert).

Turn on the free parity features

Vercel featureCloudflare setting
Edge CDNProxied DNS (orange cloud) + Speed → Optimization → Brotli, Auto Minifyfree
Firewall / WAFSecurity → WAF → enable Managed Ruleset; add rate-limiting rulefree
DDoS protectionOn by default when proxied; Under Attack Mode toggle for emergenciesfree
Web AnalyticsAnalytics → 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 metricsfree
Cache static assets hard Next.js fingerprints /_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:

PortSourceWhy
22 (SSH)Your IP onlyadmin access
80 / 443All IPv4/IPv6web traffic (ideally Cloudflare IP ranges only)
8000Your IP onlyCoolify 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
Docker can bypass UFW Docker writes its own iptables rules, so a published container port may be reachable even if UFW "denies" it. Treat the DigitalOcean Cloud Firewall as the authoritative layer and keep UFW as backup. Never publish app ports directly — let Traefik handle 80/443.
Don't lock yourself out Confirm your SSH IP is correct before 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:

  1. Coolify → + New → Application → Docker Imageghcr.io/umami-software/umami:postgresql-latest.
  2. Give it a Postgres DB (add a small Coolify-managed Postgres, or a new DB on the Supabase box).
  3. Set env: DATABASE_URL, APP_SECRET (any random string).
  4. Assign a domain e.g. analytics.boommedia.us → SSL auto-issues.
  5. 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)

Uptime Kuma free
One-click in Coolify. Pings every domain, alerts (email/Slack) on downtime. Cheap, high value.
Sentry free tier
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

The key insight All SaaS apps, client sites and users share one Supabase Postgres. So a single hourly dump of that database backs up every tenant at once — no separate job per app or client. Restoring one client/user is a logical copy out of that dump.

What gets backed up

LayerMethodFrequency
Supabase Postgres (all data)pg_dump → Backblaze B2 offsiteHourly
Uploaded files / Storagerclone sync volume → B2Daily
Whole dropletDigitalOcean snapshotWeekly
App codealready in GitHubon 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.

Untested backup = no backup Once a month, pull the latest dump from B2, restore into a throwaway Postgres, and confirm row counts. Log the test.
  • 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)