Cloudflare Tunnel for Self-Hosted Supabase: No Port Forwarding

Expose self-hosted Supabase securely with Cloudflare Tunnel—no public IP, no open ports, no port forwarding. Covers Realtime WebSockets and Zero Trust.

Cover Image for Cloudflare Tunnel for Self-Hosted Supabase: No Port Forwarding

You spun up a self-hosted Supabase instance on a home server, a box behind CGNAT, or a VPS where you'd rather not advertise the public IP. Now you need your app to reach it over HTTPS—but you don't want to open ports, run a dynamic DNS hack, or expose port 5432 to the entire internet's bot traffic. This is one of the most common questions in r/selfhosted, and the cleanest answer in 2026 is a Cloudflare Tunnel.

A Cloudflare Tunnel gives you a public, TLS-terminated hostname for your self-hosted Supabase deployment without inbound firewall rules. Your server makes an outbound-only connection to Cloudflare's edge, and traffic rides that connection back home. This guide covers the setup, the gotchas specific to Supabase (Realtime WebSockets and the Studio dashboard, especially), and where it fits relative to a traditional reverse proxy.

How a Cloudflare Tunnel Works (and Why It's Different)

A normal production setup puts Nginx, Traefik, or Caddy in front of Supabase, terminates SSL there, and requires you to open port 443 to the world. That works great when you have a public IP and control of your firewall.

A Cloudflare Tunnel inverts the model. You install a lightweight daemon called cloudflared on the same host as Supabase. It opens a persistent outbound connection to Cloudflare's network. When a request hits api.yourdomain.com, Cloudflare routes it down that existing connection to your server—no inbound ports, no static IP required.

That makes it ideal for:

  • Home labs and servers behind CGNAT or a residential router you don't control
  • VPS deployments where you want Cloudflare's DDoS protection and WAF in front of everything
  • Keeping your origin IP completely hidden

The trade-off is honest: all your traffic now flows through Cloudflare, you're adding a dependency on their edge, and free-plan request/upload limits can bite on large Storage transfers. For most small-to-medium self-hosters, that's a fair deal. For high-throughput Storage workloads, weigh it against a plain reverse proxy.

Step 1: Create the Tunnel

Install cloudflared on your Supabase host and authenticate it against the domain you've added to Cloudflare:

# Install (Debian/Ubuntu)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

# Authenticate and create the tunnel
cloudflared tunnel login
cloudflared tunnel create supabase

This produces a tunnel ID and a credentials JSON file. Most people now manage the tunnel from the Zero Trust dashboard (Networks → Tunnels) instead of local config files, since it survives reinstalls and lets you edit routes without SSH. Either approach works; the dashboard is easier to live with.

Step 2: Route Traffic to Kong, Not the Raw Services

The single most important decision: point the tunnel at the Kong API gateway (port 8000), not at individual containers. Kong is the front door for the REST API, Auth, Storage, and Realtime in the standard docker-compose setup. Routing everything through it keeps your public surface to one hostname and one port.

In your tunnel config, map a public hostname to Kong:

# config.yml
tunnel: <TUNNEL_ID>
credentials-file: /root/.cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: api.yourdomain.com
    service: http://localhost:8000
  - service: http_status:404

If you run cloudflared itself as a Docker container in the same Compose stack, add network_mode: host (or attach it to the Supabase network and use the Kong service name instead of localhost). This is the step people most often get wrong—without host networking, localhost:8000 inside the cloudflared container points at the container, not at Kong, and every request returns a connection refused. After updating SITE_URL and API_EXTERNAL_URL to your new hostname, restart the stack so the environment variables take effect.

Step 3: Don't Break Realtime WebSockets

Supabase Realtime is a Phoenix/Elixir server that pushes Postgres changes over WebSockets. Cloudflare proxies WebSockets automatically on all plans, so the connection upgrade works out of the box through a Tunnel—but two things still trip people up:

  1. Idle timeouts. Cloudflare closes proxied WebSocket connections after a period of inactivity. Make sure your Supabase client has reconnection enabled (it does by default) and that your app sends periodic heartbeats. The supabase-js Realtime client handles this, but custom clients may not.
  2. The client URL. Your frontend must connect to wss://api.yourdomain.com/realtime/v1 through the same Kong hostname—not a separate subdomain pointed straight at port 4000. Keep everything behind Kong and the Tunnel handles the rest.

If channels connect but never receive payloads, the problem is almost always upstream of Cloudflare: check that logical replication and the Realtime container are healthy. Our Realtime configuration guide covers those failure modes in detail.

Step 4: Lock Down Studio with Zero Trust

Here's the part too many tutorials skip. The Studio dashboard (port 3000) gives full administrative access to your database. You should never expose it through a plain public hostname—Tunnel or not.

Cloudflare Tunnel pairs naturally with Zero Trust Access to fix this. Create a separate hostname like studio.yourdomain.com routed to http://localhost:3000, then put an Access application in front of it:

  1. In the Zero Trust dashboard, go to Access → Applications → Add → Self-hosted.
  2. Set the application domain to studio.yourdomain.com.
  3. Add a policy: allow only your email addresses (the built-in one-time PIN works immediately), or wire up Google/GitHub SSO. You can also restrict by source IP.

Now Studio is gated behind an identity check before a single byte reaches your server. Apply the same pattern to any other admin surface. This is a far stronger posture than IP allowlisting alone, and it complements the rest of a production hardening checklist. Note that your PostgreSQL port (5432) should never go through the Tunnel at all—keep direct database access on a private network, VPN, or SSH tunnel.

Step 5: Run cloudflared as a Service

Once the tunnel works, install it as a system service so it survives reboots:

sudo cloudflared service install
sudo systemctl enable --now cloudflared

Or, if you manage it from the dashboard, copy the generated docker run command and add --network host so the container can reach Kong on localhost. Either way, verify the tunnel shows HEALTHY in the Zero Trust dashboard before you point production traffic at it.

Tunnel vs. Reverse Proxy: Which Should You Use?

FactorCloudflare TunnelReverse Proxy (Nginx/Caddy)
Public IP requiredNoYes
Open inbound portsNone80/443
Origin IP hiddenYesNo (unless proxied)
SSL certificatesManaged by CloudflareLet's Encrypt / your own
Large Storage uploadsSubject to CF limitsNo added limit
External dependencyCloudflare edgeNone

There's no universal winner. If you have a clean VPS with a public IP and want maximum control over throughput, a reverse proxy with automated SSL is simpler and faster. If you're behind CGNAT, want your IP hidden, or want Cloudflare's WAF in front of everything, the Tunnel is hard to beat. Plenty of teams run both—Tunnel for the admin plane, reverse proxy for high-volume API traffic.

Where Supascale Fits

A Cloudflare Tunnel solves exposure. It doesn't solve the rest of running self-hosted Supabase—provisioning services, configuring custom domains and OAuth, scheduling backups, and restoring when something breaks. That's where Supascale comes in. It gives you a management UI for custom domains with free SSL, OAuth provider configuration, and automated S3 backups with one-click restore—so your Tunnel becomes the network layer on top of a deployment that's already managed, not a pile of hand-edited Compose files.

For one payment of $39.99 and unlimited projects, it removes most of the operational toil that makes self-hosting feel heavier than it should. See the pricing page for the full breakdown.

Conclusion

A Cloudflare Tunnel is the cleanest way to expose self-hosted Supabase without port forwarding, a static IP, or an exposed origin. The recipe: route the tunnel at Kong (port 8000), keep Realtime WebSockets behind that same hostname, gate Studio behind Zero Trust Access, and never tunnel raw PostgreSQL. Pair it with proper backups and domain management and you have a setup that's both reachable and defensible.

Ready to spend less time wrestling Compose files and more time shipping? Start with Supascale and let the platform handle deployment, backups, and domains while Cloudflare handles the edge.

Further Reading