Deploying an application on my own VPS usually ends with the same checklist:

  • point a domain at the server;
  • open ports 80 and 443;
  • configure the reverse proxy;
  • issue an HTTPS certificate;
  • verify that the firewall is not blocking the request.

None of those steps is unusual. But sometimes I do not want the server to accept public web traffic at all.

Maybe the VPS is behind NAT. Maybe the network does not allow inbound ports. Maybe I want requests to pass through Cloudflare Access before they can reach an internal dashboard. Or maybe I simply do not want the server's public IP to provide a second route around the protection in front of it.

This is the problem behind Cloudflare Tunnel support in Nitrobase.

Nitrobase has only recently launched and is still in Beta. This feature is not an attempt to hide how networking works. Its purpose is to make one alternative deployment path repeatable: connect outward from the VPS, route traffic through a private proxy entrypoint, and keep ports 80 and 443 closed when the server does not need them.

The before-and-after picture

With a traditional public route, the browser connects directly to the VPS:

🌍 Browser │ │ HTTPS :443 ▼ ┌──────────────────── VPS ────────────────────┐ │ Public port 443 │ │ │ │ │ ▼ │ │ Traefik ───────────────► Application │ └─────────────────────────────────────────────┘ 

With a tunnel-only route, the direction of the first connection changes:

🌍 Browser │ ▼ ☁️ Cloudflare edge │ │ Existing outbound tunnel ▼ ┌──────────────────── VPS ────────────────────┐ │ cloudflared │ │ │ private Docker network │ │ ▼ │ │ Traefik :8081 ──────────► Application │ │ │ │ Public :80 / :443 ✕ closed │ └─────────────────────────────────────────────┘ 

The VPS does not wait for a new inbound connection on a public web port. The cloudflared connector running on the server establishes an outbound connection to Cloudflare, and requests return through that connection.

That is the main idea. The details around the private hop are what make it useful in practice.

Why not just point the tunnel directly at every container?

An application can contain several services. Supabase, Appwrite, and many Docker Compose projects are not represented by one container and one port.

Nitrobase already uses Traefik to decide which domain belongs to which service. Recreating that routing logic inside every tunnel hostname would give me two separate routing configurations to maintain.

Instead, Nitrobase gives Traefik a private tunnel entrypoint. Cloudflare forwards a hostname to:

http://nitrobase-traefik:8081 

Traefik then applies the route associated with the requested domain and forwards the request to the correct application service.

The path becomes:

hostname → Cloudflare Tunnel → private Traefik route → application 

The public and tunnel paths can therefore use the same application-level routing model without exposing the tunnel entrypoint on the host.

The isolation detail I care about most

The cloudflared container does not need to join every application's Docker network.

It joins a dedicated private network shared with Traefik. From there, Traefik performs the final hop to the application.

Component What it can reach
Cloudflare edge The registered tunnel connector
cloudflared The private Traefik tunnel entrypoint
Traefik Applications with configured routes
Application container Only the networks assigned to that application

This separation keeps the connector focused on one job. It carries requests to the proxy; it does not become a container with direct access to every backend service.

Tunnel-only mode closes the second door

Cloudflare Access can protect a hostname, but that protection only applies to traffic that passes through Cloudflare.

If the same application remains reachable through the VPS public IP, a visitor may be able to bypass that layer by using the origin directly. The exact risk depends on the domain, proxy rules, and firewall, but the architecture now has two possible doors:

Door A: Cloudflare → Access policy → Tunnel → Application Door B: Public VPS IP → Traefik → Application 

Nitrobase provides a Tunnel only option for servers that should use only Door A. In this mode, Traefik keeps the private tunnel entrypoint but does not publish ports 80 and 443 on the host.

Door A: ✅ available Door B: 🔒 not published 

That is more meaningful than adding a tunnel while leaving the original public path untouched.

What Nitrobase configures—and what it deliberately does not

I chose a connector-token flow rather than asking Nitrobase for broad Cloudflare account access.

The setup is split between the two systems:

In Cloudflare

  1. Create a remotely managed tunnel.
  2. Copy its connector token.
  3. Add a public hostname for the application domain.
  4. Set its service to http://nitrobase-traefik:8081.

In Nitrobase

  1. Open Server → Settings → Cloudflare Tunnel.
  2. Paste the connector token.
  3. Enable the tunnel.
  4. Optionally enable Tunnel only.
  5. Mark the application domain as Via Cloudflare Tunnel.

Nitrobase does not call the Cloudflare API to create tunnels, DNS records, or Access policies. It stores the connector token encrypted, starts the cloudflared container, and configures the private routing path on the VPS.

I like this boundary because the permission is narrow: the token connects one tunnel. It is not an API key with control over the rest of the account.

A small but important status check

A running cloudflared container does not necessarily mean the tunnel has connected successfully.

A revoked or incorrect token may leave the process alive while it repeatedly fails to register with the Cloudflare edge. This is similar to the monitoring problem I wrote about earlier: running is not the same as healthy.

Nitrobase checks the connector's readiness endpoint and exposes states such as:

  • connecting;
  • connected;
  • failing;
  • stopped;
  • not configured.

This helps distinguish “the container exists” from “the edge can actually send traffic through it.”

Public ports or a tunnel?

Neither option is universally better.

Question Public Traefik ports Cloudflare Tunnel
Requires inbound 80/443 Yes No
Works without Cloudflare Yes No
Can hide the origin web ports No Yes, with tunnel-only mode
Adds an external network dependency No Yes
Fits Cloudflare Access Possible, but origin bypass needs care Natural fit
Simplest path for a public website Usually Not always

For a public site where direct control and fewer dependencies matter most, ordinary HTTPS through Traefik may remain the simpler choice.

For an internal dashboard, a homelab behind NAT, or a server where public inbound ports are undesirable, the tunnel path can solve a very specific problem cleanly.

My pre-flight checklist

Before I switch an application to tunnel-only mode, I check these items:

  • The tunnel reports Connected.
  • The public hostname points to http://nitrobase-traefik:8081.
  • The domain is marked Via Cloudflare Tunnel in Nitrobase.
  • The application answers correctly through its public hostname.
  • Any Cloudflare Access policy behaves as expected.
  • I have another way to reach the VPS, usually SSH.
  • Only then do I disable the public web ports.

That order matters. Closing the original route before verifying the tunnel turns a networking improvement into an avoidable outage.

What this feature is trying to solve

The feature is not about declaring tunnels more secure than every public reverse proxy. Security depends on the full configuration, not the presence of one component.

The problem is more concrete:

How can I publish an application from my own VPS when direct inbound web traffic is unavailable or unwanted, without maintaining a second set of routes for every container?

Nitrobase uses Cloudflare Tunnel as the transport, a private Traefik entrypoint as the routing boundary, and tunnel-only mode to remove the public web path when it is no longer needed.

It is still a Beta feature. Different firewalls, VPS networks, Cloudflare policies, and application behaviors will expose edge cases that need more testing.

But the outcome I want is simple: the application remains on infrastructure I control, while the path into it matches the network boundary I actually want.

Nitrobase source code: https://github.com/AydenGroup/nitrobase