How to Expose Your Docker Apps to the Internet with Cloudflare Tunnels
The complete guide to running production Docker apps on a local machine with Cloudflare Tunnels — no open ports, no static IP, no VPS needed.
I run 14 production apps on a Mac mini in my office.
Not on AWS. Not on Render. Not on DigitalOcean.
On a used Mac mini. Accessible from anywhere, with HTTPS, on custom subdomains. All for the price of a domain renewal.
The thing making this possible: Cloudflare tunnels.
If you're self-hosting Docker containers, this is the cleanest way to get them onto the internet — without opening firewall ports, configuring a static IP, or paying for a VPS just to run a reverse proxy. Here's the exact setup I use.
What Is a Cloudflare Tunnel?
A Cloudflare tunnel creates an outbound-only connection from your machine to Cloudflare's edge network. Your apps don't need an inbound port open. Your IP doesn't need to be static. Cloudflare handles routing, HTTPS, and DNS automatically.
The process: install a small daemon (cloudflared) on your machine, create a tunnel in the Cloudflare dashboard, and map subdomains to your localhost ports. That's it.
What You Need
- A domain on Cloudflare (DNS management is free — you can transfer any existing domain)
- Cloudflare Zero Trust account (free tier covers this completely)
- Docker and Docker Compose on your machine
- About 20 minutes
Step 1: Create a Cloudflare Zero Trust Account
Go to one.dash.cloudflare.com, sign in, and navigate to Zero Trust. Create a new organization if prompted — the free tier works fine for self-hosting.
Step 2: Create the Tunnel
In the Zero Trust dashboard: Networks > Tunnels > Create a tunnel. Name it whatever you want (I use "homelab"), select "Cloudflared" as the connector type. Cloudflare generates a token — copy it.
Step 3: Add cloudflared to Your Docker Compose
The cleanest approach is running cloudflared as a container alongside your other apps. Add this service to your docker-compose.yml:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOU...N}
networks:
- your_networkAdd your token to .env:
CLOUDFLARE_TUNNEL_TOKEN=your-t...nRun `docker compose up -d cloudflared`. The tunnel connects and shows healthy in the dashboard within a few seconds.
## Step 4: Map Subdomains to Your Apps
In the Cloudflare dashboard, go to your tunnel > Public Hostname > Add a hostname. One entry per app:
| Subdomain | Domain | Service |
|-----------|--------|---------|
| n8n | yourdomain.com | http://localhost:5678 |
| crm | yourdomain.com | http://localhost:3000 |
| docs | yourdomain.com | http://localhost:3001 |
Cloudflare automatically creates DNS records and provisions SSL certificates. No certbot. No Nginx config. No manual DNS editing.
## Step 5: Test It
Hit `https://n8n.yourdomain.com` from a different network (your phone on LTE works). If the tunnel is healthy and the container is running, it loads.
That's the complete setup.
## My Real-World Stack
Here's what I'm running through a single tunnel on one machine:
* **Twenty** — CRM (port 3000)
* **n8n** — automation (port 5678)
* **Postiz** — social scheduling (port 3000)
* **NocoDB** — internal databases (port 8080)
* **Outline** — team docs (port 3000)
* **Listmonk** — newsletters (port 9000)
* **Formbricks** — forms and surveys (port 3000)
* **Documenso** — contract signing (port 3000)
* **InvoiceNinja** — billing (port 80)
* And five others
All accessible at `*.whtnxt.io`. All HTTPS. One machine. One tunnel.
Monthly infrastructure cost: domain renewal.
## A Few Things to Know Before You Commit
**Your machine needs to stay on.** The tunnel drops when the machine sleeps. On macOS, disable sleep in System Settings > Energy. A dedicated Mac mini or a cheap NUC beats a laptop for this — lower power draw, no screen, designed to run 24/7.
**Cloudflare Access gives you a login gate.** If you want email-based or SSO authentication in front of any app — without touching the app itself — Zero Trust > Access > Applications handles it. It's the most underused feature of this whole setup.
**This doesn't replace backups.** The tunnel handles connectivity, not redundancy. Back up your Docker volumes to an external drive or a cloud bucket. Separate concern.
**Performance is fine for internal tools.** I wouldn't route a high-traffic public marketing site this way. For CRM, automation platforms, internal dashboards, and ops tools — completely solid. No noticeable latency.
## The Alternative I Walked Away From
For two years I ran a VPS with Caddy as a reverse proxy — a $6/month DigitalOcean droplet managing subdomains and SSL certificates. It worked. It also required updates, broke twice during Caddy upgrades, and added one more thing to think about.
The tunnel approach trades a VPS bill for zero infrastructure overhead. No server to SSH into when something breaks at midnight. For a solo operator, that trade is simple.
What are you self-hosting that you've been meaning to expose properly?