Skip to content
All projects

Self-hosted infrastructure

Three VPS, one PaaS, and everything on this site runs on it

Layout

  • dp1 runs Dokploy, a self-hosted PaaS on top of Docker and Traefik. Every application on this site (the portfolio, spillpromo’s web and worker services, Omnivetted) is a Dokploy application or Compose project that redeploys on a push to main. Build logs, environment variables and health checks live there.
  • dp2 runs a self-hosted Supabase instance (Postgres, Auth, Storage, PostgREST) used by both products. Postgres and the API gateway only accept connections from dp1’s IP.
  • A third machine hosts a Linux desktop over xrdp for remote work, reachable only through Guacamole on dp1, with the RDP port firewalled to dp1’s address and the Guacamole page behind Cloudflare Access.

All public traffic enters through Cloudflare. Besides DNS and caching, two things matter: Security rules decide which Supabase paths are reachable from the internet (the auth, REST and storage endpoints are; everything else is not), and Cloudflare Access puts an identity check in front of every admin panel, so Dokploy, the monitoring hub and the remote desktop never face the open internet with just a password.

Monitoring and backups

Beszel gives per-host and per-container CPU, memory, disk and bandwidth with history and Telegram alerts. The agents on dp2 and the third machine connect outbound to the hub over WebSocket, so none of them need an inbound port open; the hub’s own port is restricted to those two IPs. External uptime checks on the public endpoints are the next thing on the list.

With a self-hosted database, backups are my problem. Postgres is dumped daily to object storage with a retention window, and the restore procedure has to be rehearsed, not just documented.

Why self-host at all

Cost, mostly, and the lessons. Two products, a portfolio and a database run on a few dollars a month instead of several managed services at $20–25 each. The trade is that I own the failure modes: a Cloudflare rule that locked out sign-in for a week, a container that would not boot because the base image shipped Node 20 and the Supabase client needed Node 22’s native WebSocket, Docker’s DOCKER-USER chain quietly bypassing the host firewall. Each of those is now a note in the repo and a reason the setup looks the way it does.

The reviews on Omnivetted about VPS providers, Cloudflare in front of a cheap VPS, and sharing one Supabase instance between two apps all come from running this.