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.

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.