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.
How it works
Section titled “How it works”cloudflaredruns as a container, dials outbound to Cloudflare- Cloudflare routes DNS for
app.example.comto their edge - Traffic from users → Cloudflare edge → the tunnel → your container
- No ports open on your router. No dynamic DNS. Works through any NAT.
Prerequisites
Section titled “Prerequisites”- 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
Step 1 — Create the tunnel
Section titled “Step 1 — Create the tunnel”Cloudflare dashboard:
- Zero Trust → Networks → Tunnels → Create tunnel
- Name:
dockmesh-home - 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.
Step 3 — Map hostnames to services
Section titled “Step 3 — Map hostnames to services”Back in Cloudflare Zero Trust:
Tunnels → dockmesh-home → Public hostnames → Add a public hostname
Example for Nextcloud:
| Field | Value |
|---|---|
| Subdomain | cloud |
| Domain | example.com |
| Type | HTTP |
| URL | nextcloud_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.
Advantages
Section titled “Advantages”- 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
Tradeoffs
Section titled “Tradeoffs”- 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
Keeping the dockmesh UI private
Section titled “Keeping the dockmesh UI private”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.
Backups
Section titled “Backups”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.
Troubleshooting
Section titled “Troubleshooting”“No healthy origin” at the Cloudflare edge:
- Check
cloudflaredcontainer 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 athttp://cloudflared:2000/metrics - Add to your Prometheus scrape config for monitoring
See also
Section titled “See also”- Reverse Proxy — the alternative: open ports + Caddy
- Home-Lab Setup — integrated setup guide
- Hardening — don’t expose the dockmesh UI publicly