There comes a point where you want to show something you built — a side project or a demo or a staging environment that only exists on your machine — and immediately you get dragged into the usual nonsense of port forwarding and router UIs that look like they were built in 2004 and firewall rules you half understand and fully distrust. Cloudflare Tunnels solve this problem quietly and elegantly and the best part is you do not need to install anything on your host because everything runs inside Docker including authentication.

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 which handles the ugly parts. No open ports and HTTPS by default and certificates that just work without certbot rituals. If you want auth Cloudflare Access is already there. It is infrastructure that stays out of your way.

You need a Cloudflare account with a domain already added and Docker and Docker Compose and a directory on your host to persist credentials like /home/troysk/.cloudflared. That directory matters because without it you will keep re-authenticating and wondering why life is so hard.

First you authenticate with Cloudflare using Docker. You are not installing cloudflared but running it in a container.

docker run --rm -it \
  -v /home/troysk/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel login

This runs cloudflared in a temporary container with a mounted host directory so credentials survive container death. You get a URL to open in your browser and when it finishes a cert.pem file appears on your host.

Next you create the tunnel.

docker run --rm -it \
  -v /home/troysk/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel create yourapp

This registers a tunnel with Cloudflare and creates a credentials JSON file locally that the running tunnel will use to authenticate itself. Do not delete it or rename it randomly.

Then you route DNS to the tunnel because a tunnel without DNS is just a philosophical exercise.

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 and there are no dashboards or manual CNAMEs to deal with.

Now you write the tunnel configuration to tell cloudflared what to do with incoming traffic. Create this file on your host at /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

The credentials file path is inside the container and app is the Docker service name not localhost. The last rule is mandatory unless you enjoy undefined behavior. This file is where most mistakes happen so read it twice.

Then you add cloudflared to Docker Compose.

services:
  app:
    image: your-app-image:latest
    container_name: your-application

  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 there is no ports section. Your app is not exposed to the host and cloudflared talks to it over Docker’s internal network which is the whole point.

Finally start everything.

docker-compose up -d

Within seconds Cloudflare reports the tunnel as connected and you open your browser and visit your subdomain and it just works. No host pollution and no open ports and 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 and share them and keep your machine boring.

Let me know what you think on Twitter. I am @troysk704.