shlewislee.me

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

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 later

Now 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: true

2. 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 -d

If 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.

Reply to this post by email ↪