Exposing Docker Compose to the world
There comes a point where you want to show something you built.
A side project.
A demo.
A staging environment that only exists on your machine.
And immediately you are dragged into the usual nonsense. Port forwarding. Router UIs that look like they were built in 2004. Firewall rules you half understand and fully distrust. 😭
This is exactly the kind of problem Cloudflare Tunnels solve quietly and elegantly.
Even better, you do not need to install anything on your host. No brew. No apt-get. No binaries littering your system. Everything runs inside Docker, including authentication.
If you already live in Docker, this fits right in.
Why Cloudflare Tunnels Work So Well
Cloudflare Tunnels flip the usual networking model on its head.
Instead of opening ports and letting the internet knock on your door, your machine initiates an outbound connection to Cloudflare. From that point on, Cloudflare handles the ugly parts.
A few reasons this matters:
-
No open ports Nothing exposed on your router. Nothing to forget later.
-
HTTPS by default Certificates just work. No certbot rituals.
-
Optional access control If you want auth, Cloudflare Access is already there.
-
Boring reliability Which is the best kind.
This is infrastructure that stays out of your way while you murmur; “This is the way”.
What You Need Before You Start
Nothing exotic.
- A Cloudflare account with a domain already added
- Docker and Docker Compose
- A directory on your host to persist Cloudflare credentials
For example:
/home/troysk/.cloudflared
This directory matters. Without it, you will keep re-authenticating and wondering why life is so hard.
Step 1: Authenticate with Cloudflare Using Docker
You are not installing cloudflared. You are running it.
docker run --rm -it \
-v /home/troysk/.cloudflared:/home/nonroot/.cloudflared \
cloudflare/cloudflared:latest tunnel login
What this does:
- Runs cloudflared in a temporary container
- Mounts a host directory so credentials survive container death
- Starts the Cloudflare login flow
You will get a URL. Open it in your browser. Log in. Pick the domain.
When this finishes, you should see a cert.pem file appear on your host inside .cloudflared.
That file is your passport.
Step 2: Create the Tunnel
Now that Cloudflare trusts you, you can create a tunnel identity.
docker run --rm -it \
-v /home/troysk/.cloudflared:/home/nonroot/.cloudflared \
cloudflare/cloudflared:latest tunnel create yourapp
Replace yourapp with something meaningful.
This does two important things:
- Registers a tunnel with Cloudflare
- Creates a credentials JSON file locally
That JSON file is what the running tunnel will later use to authenticate itself.
Do not delete it. Do not rename it randomly.
Step 3: Route DNS to the Tunnel
A tunnel without DNS is just a philosophical exercise.
You now tell Cloudflare which hostname should point to this tunnel.
docker run --rm -it \
-v /home/troysk/.cloudflared:/home/nonroot/.cloudflared \
cloudflare/cloudflared:latest tunnel route dns yourapp yoursubdomain.yourdomain.com
Cloudflare creates the DNS record for you.
No dashboards. No manual CNAMEs. Just done.
Step 4: Write the Tunnel Configuration
Now we tell cloudflared what to do with incoming traffic.
Create this file on your host:
/home/troysk/.cloudflared/yourapp.yml
tunnel: yourapp
credentials-file: /etc/cloudflared/YOUR_TUNNEL_UUID.json
ingress:
- hostname: yoursubdomain.yourdomain.com
service: http://app:80
- service: http_status:404
A few things worth slowing down for:
- The credentials file path is inside the container
appis the Docker service name, not localhost- The last rule is mandatory unless you enjoy undefined behavior
- The port on which your app serves, port 80 in this example
This file is where most mistakes happen. Read it twice.
Step 5: Add Cloudflared to Docker Compose
This is where everything comes together.
services:
app:
image: your-app-image:latest
container_name: your-application
# No ports exposed
cloudflared:
image: cloudflare/cloudflared:latest
container_name: yourapp-cloudflared
restart: unless-stopped
command: tunnel --config /etc/cloudflared/yourapp.yml run yourapp
volumes:
- /home/troysk/.cloudflared:/etc/cloudflared
depends_on:
- app
Notice what is missing.
No ports: section.
Your app is not exposed to the host. Cloudflared talks to it over Docker’s internal network.
This is the whole point.
Step 6: Start Everything
docker-compose up -d
Within seconds, Cloudflare will report the tunnel as connected.
Open your browser.
Visit yoursubdomain.yourdomain.com.
It should just work.
Closing Thoughts
This is one of those setups that feels obvious once you see it.
No host pollution. No open ports. No security theatre.
Just containers talking to containers, with Cloudflare acting as the boring adult in the room.
If you are already Docker-first, this is the cleanest way I know to expose something to the internet without regretting it later.
Build things. Share them. Keep your machine boring.
This is the way!