Skip to content

Expose Services via Cloudflare Tunnel

Cloudflare Tunnel (formerly Argo Tunnel) creates an outbound-only connection from your server to Cloudflare’s edge. You can serve app.example.com to the internet without opening ports, forwarding anything on your router, or exposing your home IP.

Perfect for home labs behind CGNAT and for paranoid self-hosters who don’t want any inbound connections.

  1. cloudflared runs as a container, dials outbound to Cloudflare
  2. Cloudflare routes DNS for app.example.com to their edge
  3. Traffic from users → Cloudflare edge → the tunnel → your container
  4. No ports open on your router. No dynamic DNS. Works through any NAT.
  • A domain managed via Cloudflare (free plan works fine)
  • The dockmesh host has outbound internet access (if not, skip this — tunnels need a connection to Cloudflare)
  • The service you want to expose is running in dockmesh

Cloudflare dashboard:

  1. Zero Trust → Networks → Tunnels → Create tunnel
  2. Name: dockmesh-home
  3. Save the token (long base64 string)

Step 2 — Deploy cloudflared as a dockmesh stack

Section titled “Step 2 — Deploy cloudflared as a dockmesh stack”

Stacks → New stack → name cloudflared:

services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN}
TUNNEL_METRICS: 0.0.0.0:2000
networks:
- default
- proxy-net
networks:
proxy-net:
external: true
name: dockmesh_proxy

${TUNNEL_TOKEN} is the token from step 1.

The proxy-net external network is the one Caddy/other stacks live on. If your stacks are on stack-local networks only, you can instead target them by Docker hostname (<project>_<service>_1).

Deploy.

Back in Cloudflare Zero Trust:

Tunnels → dockmesh-home → Public hostnames → Add a public hostname

Example for Nextcloud:

FieldValue
Subdomaincloud
Domainexample.com
TypeHTTP
URLnextcloud_app_1:80

(Or http://nextcloud.home:80 if you use container aliases.)

Cloudflare automatically creates the cloud.example.com CNAME pointing to the tunnel. Within ~30 seconds, https://cloud.example.com works from anywhere in the world.

Repeat for each service you want public.

  • No open ports — your firewall can deny all inbound
  • Real TLS certs via Cloudflare — no Let’s Encrypt rate limits, no ACME challenges to set up
  • DDoS protection from Cloudflare’s network for free
  • Works behind CGNAT (ISPs that give you a shared public IP)
  • Works from a laptop in a coffee shop for quick demos
  • All traffic flows through Cloudflare — they see HTTPS-terminated content
  • Cloudflare can decrypt responses at their edge (by design — that’s how caching and WAF work). If this is a problem for your threat model, don’t use this pattern
  • Free plan has a 100 MB upload limit per request — uploading large files (Nextcloud, Mastodon media) needs paid plan or a different approach
  • Outbound bandwidth counts against your home ISP’s allowance

Don’t add a public hostname for the dockmesh UI itself. Administrators access the UI over Tailscale or direct LAN — never publicly, even with auth.

The tunnel token is the main thing to back up here. If lost, you generate a new tunnel. But any public hostnames tied to the old tunnel break — easier to just keep the token.

cloudflared has no persistent state to back up.

“No healthy origin” at the Cloudflare edge:

  • Check cloudflared container is running: Containers → cloudflared_cloudflared_1
  • Verify network connectivity to target: docker exec -it cloudflared_cloudflared_1 wget -O - http://nextcloud_app_1:80

WebSocket issues (e.g. Mastodon streaming):

  • Make sure the hostname is HTTP (not HTTPS) — the tunnel terminates TLS at Cloudflare; inside your network it’s plain HTTP
  • Cloudflare supports WebSockets automatically — no extra config

Disconnections:

  • Check TUNNEL_METRICS — the cloudflared metrics endpoint exposes tunnel health at http://cloudflared:2000/metrics
  • Add to your Prometheus scrape config for monitoring