Assistance trying to make Caddyfile for reverse proxy?

1. The problem I’m having:

I’ve been thinking I’d like to switch away from SWAG to Caddy. Caddy is installed in a docker container on Debian(running Caddy on the same ports as SWAG, so I’m shutting down one before I start the other).

I have a domain running through Cloudflare DNS (A record for domain.com, CNAME for *), with a few docker containers I’m exposing as well as my HomeAssistant VM. I’ve got Heimdall running on domain.com and www.domain.com, then I’ve got photos.domain.com, recipes.domain.com, and so on.

I want to use Maxmind and Crowdsec. I want to limit access to Canada only. I’d like to disallow access to anyone connecting direct to the IP instead of the domain/URL.

I also don’t want to allow any access to anything undefined. As an example, I have nothing on `dev.domain.com, so that should return a 404 or maybe even a 444.

I’ve been tinkering with this on-and-off for a few days, but I’m really struggling with building the Caddyfile correctly and could use some help. It’s like all the information is 98% available. Like the Cloudflare DNS module tells me what needs to be in the Caddyfile, but I can’t for the life of me figure out where.

2. Error messages and/or full log output:

Right now, the errors are me trying to figure out how to use Maxmind, Crowdsec, and Cloudflare DNS in the Caddyfile - where to put it all, how to format it, and how to keep the API keys out of the Caddyfile directly.

[caddy] 2026-05-11T16:26:13.580131383Z Error: adapting config using caddyfile: parsing caddyfile tokens for 'tls': getting module named 'dns.providers.cloudflare': module not registered: dns.providers.cloudflare, at /etc/caddy/Caddyfile:21 import chain ['/etc/caddy/Caddyfile:26 (import cloudflare)']

3. Caddy version:

v2.11.2 h1:iOlpsSiSKqEW+SIXrcZsZ/NO74SzB/ycqqvAIEfIm64=

4. How I installed and ran Caddy:

Caddy is running as a docker container on a Debian VM via docker compose. Crowdsec is also running alongside it. I’m using a dockerfile to try to add Crowdsec, Maxmind, and Cloudflare DNS to the Caddy container.

a. System environment:

Docker on a Debian x64 VM

b. Command:

docker compose up -d

c. Service/unit/compose file:

Docker Compose:

services:
  caddy:
    #image: caddy:latest
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: caddy
    restart: unless-stopped
    depends_on:
      crowdsec:
        condition: service_healthy
    environment:
      - CROWDSEC_API_KEY=${CROWDSEC_API_KEY}
    ports:
      - 81:80
      - 444:443
      - 444:443/udp
    volumes:
      - /opt/docker/appdata/Caddy/conf:/etc/caddy
      - /opt/docker/appdata/Caddy/site:/srv
      - /opt/docker/appdata/Caddy/data:/data
      - /opt/docker/appdata/Caddy/config:/config
      - /opt/docker/appdata/Caddy/logs:/var/log/caddy
      - /opt/docker/appdata/Caddy/maxmind:/maxmind
    networks:
    - crowdsec
    security_opt:
      - no-new-privileges=true

  crowdsec:
    image: docker.io/crowdsecurity/crowdsec:latest
    container_name: crowdsec
    environment:
      - GID=1000
      - COLLECTIONS=crowdsecurity/caddy crowdsecurity/http-cve crowdsecurity/whitelist-good-actors
      - BOUNCER_KEY_CADDY=${CROWDSEC_API_KEY}
    volumes:
      - /opt/docker/appdata/Crowdsec/db:/var/lib/crowdsec/data/
      - /opt/docker/appdata/Crowdsec/config/acquis.yaml:/etc/crowdsec/acquis.yaml
      - /opt/docker/appdata/Caddy/logs:/var/log/caddy:ro
    networks:
      - crowdsec
    restart: unless-stopped
    security_opt:
      - no-new-privileges=true
    healthcheck:
      test:
        - CMD
        - cscli
        - lapi
        - status
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

networks:
  crowdsec:
    driver: bridge

Dockerfile:

ARG CADDY_VERSION=2

FROM caddy:${CADDY_VERSION}-builder-alpine AS builder

RUN xcaddy build \
    --with github.com/mholt/caddy-l4 \
    --with github.com/caddyserver/transform-encoder \
    --with github.com/hslatman/caddy-crowdsec-bouncer/http@main \
    --with github.com/hslatman/caddy-crowdsec-bouncer/layer4@main \
    --with github.com/porech/caddy-maxmind-geolocation \
    --with github.com/caddy-dns/cloudflare

FROM caddy:${CADDY_VERSION} AS caddy

WORKDIR /

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

d. My complete Caddy config:

This is almost undoubtedly incorrect and full of inefficiencies. I’m welcoming any and all comments and adjustments.

{
        email email@outlook.com
        debug
        crowdsec {
                api_url http://crowdsec:8080
                api_key {$CROWDSEC_API_KEY}
        }
}

(geoip) {
        @allowca {
                maxmind_geolocation {
                        db_path /maxmind/GeoLite2-Country.mmdb
                        allow_countries CA
                }
        }
}

(cloudflare) {
        tls {
                dns cloudflare {$CLOUDFLARE_API_KEY}
        }
}

domain.com, www.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca heimdall:80
}
photos.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca immich-server:2283
}
assist.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca 192.168.0.35:8123
}
seerr.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca seerr:5055
}
recipes.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca mealie:9000
}

foundry.domain.com {
        import cloudflare
        import geoip
        reverse_proxy @allowca foundryvtt:30000
}

*.domain.com {
        abort
}

5. Links to relevant resources:

I’ve been attempting to use the Caddyfile Concepts page, as well as the Github readme for the Cloudflare-DNS module.

Apologies if this sounds like noob questions, but it’s been a while since I’ve struggled this much with selfhosting/homelabbing software. Part of me wants to stick with SWAG, but if I can get this sorted it seems like it will be better, though I’ll kinda miss the SWAG dashboard.

Caddy doesn’t come with the cloudflare module, you need to make a custom build of Caddy with that module if you need to use it.

You quoted a Dockerfile, if you still get that error then you’re not actually using the binary you think you are. Make sure to do docker compose build and docker compose down before bringing it back up.

Ah, crap, you’re correct. I missed recreating rebuild when I added Cloudflare after I’d already added the others. Though now I’m having trouble with the API token variables not being pulled from the .env(maybe they can’t do that when using docker?), or maybe it’s more to do with how I’m trying to define cloudflare and call it in all the other blocks?

[caddy] 2026-05-11T20:10:49.303877356Z Error: adapting config using caddyfile: parsing caddyfile tokens for ‘tls’: missing API token, at /etc/caddy/Caddyfile:21 import chain [‘/etc/caddy/Caddyfile:26 (import cloudflare)’]

You can load .env in docker compose.

For secrets, use {env.CLOUDFLARE_TOKEN} rather than {$CLOUDFLARE_TOKEN}. See Caddyfile Concepts — Caddy Documentation