How to Expose a Service Safely with Cloudflare Tunnel (No Open Ports, No Public IP Needed)

Cloudflare Tunnel: public site, zero open ports — Works behind CGNAT. HTTPS and DDoS cover included.

Hosting a public website from home means forwarding ports 80 and 443 (tutorials 26–28) and having a public IP to forward them on — which CGNAT customers don't. Cloudflare Tunnel turns it inside out: a small cloudflared container on your server makes an outbound connection to Cloudflare, and Cloudflare forwards visitors to it. No port forward, no public IP, your home IP never appears in DNS, and Cloudflare's HTTPS, caching and DDoS protection come free. For a blog, a status page or a WordPress site it's the easiest public hosting there is.

Quick reference

TaskWhat
Needsa domain whose DNS is on Cloudflare (free plan is fine)
CreateCloudflare dashboard → Zero Trust → Networks → Tunnels → Create
Runcloudflare/cloudflared container with the tunnel token
Map a hostnamePublic Hostname → Add: blog.example.com → http://wordpress:80
Add a loginAccess → Applications → policy: your email
Not forJellyfin/Plex streaming, large file sync, non-HTTP (SSH, game servers)

1. Create the tunnel

Cloudflare dashboard → Zero Trust (free plan) → Networks → Tunnels → Create a tunnel → Cloudflared. Name it homeserver. It shows a Docker command with a long token; copy just the token.

2. Run cloudflared

mkdir -p ~/docker/cloudflared && cd ~/docker/cloudflared
nano compose.yaml

services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
command: tunnel run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN}
networks: [proxy]
restart: unless-stopped

networks:
proxy:
external: true

Token in .env. The proxy network (tutorial 101) is how cloudflared reaches your apps by name without any published ports — add networks: [proxy] to each app it should serve.

docker compose up -d

The dashboard shows the tunnel as Healthy. Nothing was opened on the router.

3. Map a hostname to a service

Tunnels → homeserver → Public Hostname → Add a public hostname:

Save. Cloudflare creates the DNS record itself. Open https://blog.example.com — live, with a padlock, in about a minute. Repeat for each app. If you already run Nginx or Caddy, point one hostname (*.example.com) at http://nginx:80 and let your proxy do the routing as before.

4. Put a login in front of private apps

For things that aren't meant for the public — Homepage, Grafana, Uptime Kuma — Access → Applications → Add → Self-hosted, the hostname, and a policy Allow: Emails ending in @yourdomain or a specific list. Visitors hit a Cloudflare login (one-time PIN by email, or Google/GitHub) before anything reaches your server. It's Authelia (tutorial 95) hosted by Cloudflare, with no config file.

5. Settings worth setting

What it's not for

Cloudflare's terms and the free plan's limits make the tunnel unsuitable for video streaming (Jellyfin, Plex), large file sync (Nextcloud with gigabytes, Immich uploads — 100 MB upload limit on free), and anything not HTTP (SSH, Minecraft, WireGuard). Those stay on the VPN or a real port forward. Websites, dashboards, small apps and APIs: perfect.

Tunnel, VPN or port forward?

WantUse
A public website or small app, no port forwardingCloudflare Tunnel
Everything on the server, privately, from your own devicesWireGuard / Tailscale
Media streaming or big transfers to the publicport forward + reverse proxy + Fail2ban/CrowdSec

Many servers run all three.

Troubleshooting

"Bad gateway" (502) on the public name. cloudflared can't reach the service: wrong container name/port, or the app isn't on the proxy network. docker compose logs cloudflared.

Redirect loop. SSL mode Flexible with an app that forces https. Set Full, or let the app accept http from the tunnel.

Tunnel "Down" after a reboot. Container didn't start or the token changed. docker compose up -d; recreate the token if it was rotated.

Real visitor IPs show as Cloudflare's. Use the CF-Connecting-IP header in the app or Nginx (tutorial 106, step 5).

Wrapping up

A tunnel in the dashboard, one container with the token, a hostname per app on a shared Docker network. Public websites from behind any router with zero open ports, HTTPS and DDoS cover thrown in, and a login page for the private ones in two clicks. Keep the heavy and the non-web stuff on the VPN.

Call us