Using Caddy and Cloudflare Tunnel with Docker compose
There are multiple reasons to avoid hosting services directly on the Internet. But when direct hosting is unavoidable, Cloudflare is probably your best shot. And when you use Cloudflare, traditionally, you’d open a port, set your IP address in Cloudflare’s DNS records, and enable their proxy to hide your IP. However, the main issue is that you still need to keep that port open for Cloudflare’s access. While you can implement firewalls and other security measures, this approach is obviously more complex than having no open ports at all.
With Cloudflare Tunnel, you don’t need to open any ports! You can set up a docker network, isolated from other networks, and use cloudflared to handle incoming traffic without exposing any ports on your host machine.
Prerequisites
- Docker installed
- Cloudflare account
- Domain name
1. Preparing Compose File
Before writing out compose file, create a network pubnet first:
1$ docker network create pubnet
2$ touch .cf-env # Also used laterNow let’s write our new compose file:
1# docker-compose.yml
2
3services:
4 caddy:
5 image: caddy:2.9
6 hostname: caddy
7 container_name: caddy
8 restart: unless-stopped
9 networks:
10 - pubnet
11 volumes:
12 - ./Caddyfile:/etc/caddy/Caddyfile
13 - ./data:/data
14 - ./config:/config
15 extra_hosts:
16 - "host.docker.internal:host-gateway"
17
18 tunnel:
19 container_name: cftunnel
20 image: cloudflare/cloudflared
21 restart: unless-stopped
22 command: tunnel run
23 networks:
24 - pubnet
25 env_file:
26 - .cf-env
27
28networks:
29 pubnet:
30 name: pubnet
31 external: true2. Setup Cloudflare Tunnel
Before running, create a tunnel from the Zero Trust website first. Choose cloudflared and copy whatever command line it gives you.
Select docker environment and copy the command line. Since we have .cf-env file to handle the token, cut out only the token from the command line and copy the token to .cf-env file like this:
1# .cf-env
2TUNNEL_TOKEN=aBc1234...3. Set up Caddyfile
The new network pubnet will now only handle public-facing services. Whenever you spin up a new docker container for public facing services, use pubnet, give it a hostname and you can easily connect the service from Caddy with Caddyfile like this:
# Caddyfile
service.example.com {
reverse_proxy service_hostname:port
}You can use host-gateway to access services running outside docker containers, such as local development servers or system services.
From the same web dashboard, go to Public Hostnames and add your public hostname. Subdomain and domain should be the same as you set up in the Caddyfile.
Since Caddy is going to handle the internal proxy, specify a service type as HTTPS and caddy. Also from advanced settings, set TLS->Origin Server Name and HTTP Settings->HTTP Host Header as service.example.com, the same as your public hostname.
You can now start up your docker compose project:
1$ docker compose up -dIf everything’s set up correctly, Caddy will automatically handle certificates for you and is now successfully proxying all your services via Cloudflare Tunnel, all while not opening a single port from docker!
Limitation
Keep in mind that using Cloudflare Tunnel means you’re bypassing your host’s firewall capabilities and relying entirely on Cloudflare and your service’s security capabilities. While this provides convenience and security through port closure, you’ll need to rely more heavily on Cloudflare’s security features and proper application-level security measures. Consider utilizing Caddy’s rate limiting module or integrating CrowdSec into your compose environment.