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
  • app is 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!