Expose docker-compose app with a secure Cloudflare Tunnel (cloudflared)

When I build containerized apps that need to be exposed on the internet, I usually need to forward ports, set up let's encrypt and reverse proxy some random port. In this blog I'll show you how to ditch all of that in favor of 1 secure Cloudflare Tunnel in a docker-compose file.

Part 1: Setting Up the App

First, let's create and configure our state of the art app, named index.html:

<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>🤓 Hello world! 👋</h1>
<p>Answer from container.</p>
</body>
</html>

We'll host it using the latest standard NGINX container. The docker-compose.yml looks like this:

version: "3"
services:
  web:
    image: nginx:latest
    volumes:
      - ./index.html:/usr/share/nginx/html/index.html
    restart: unless-stopped

Notice how we are not exposing port 80 to the host!

Part 2: Setting Up the Cloudflare Tunnel

To set up a Cloudflare Tunnel for your app, you can follow the instructions in this guide: Creating a Tunnel through the Cloudflare Dashboard. Navigate to one.dash.cloudflare.com > Access > Tunnels and click the "Create" button.

When prompted, specify the hostname as http://web:80. The web refers to the service named web in our docker-compose.yml file.

The settings screen for configuring a hostname for a Cloudflare Tunnel.
The tunnel should be mapped to http://web:80.

Once the tunnel is created, click the "Configure" button and scroll down to find the Tunnel token. Copy this token as you will need it in the next steps.

Part 3: Include the tunnel as a service

Now that we've created our tunnel, we can configure the tunnel on our server side. Let's create a tunnel.env file to separate the token from our docker-compose.yml file:

TUNNEL_TOKEN=<PASTE_TOKEN_HERE>

Our compose file could end up in a repository, our .env file should only live on the server.

And now we can add the cloudflared service to our docker-compose like this:

version: "3"
services:
  web:
    image: nginx:latest
    volumes:
      - ./index.html:/usr/share/nginx/html/index.html
    restart: always
    container_name: web

  tunnel:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run
    env_file: tunnel.env
    restart: always
    container_name: tunnel
    depends_on:
      - web

Now you can run the stack with docker compose up -d.

Conclusion

Cloudflare. Tunnels. Are. Easy! And they are free. What I like about this setup:

  • A tunnel is a secure way of communication between Cloudflare and your application.
  • You don't need to setup port-forwarding for this to work. No more adding your IP to DNS.
  • You don't need a reverse proxy for this to work.
  • You don't need Let's Encrypt certificates for this to work.
  • You don't even need to expose any ports to your host (which I super love 😍).
  • You can create a specific tunnel per application by just adding the cloudflared service to your Docker Compose stack.

Changelog

  • 2023-05-14 Added container names for more predictable naming of the stack (without the numbering). Removed quotation from the YAML files.
expand_less