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.