Can I deduplicate/streamline my dual-domain Caddyfile?

1. The problem I’m having:

I have a single Caddy server that is available over two domains (one domain’s nameservers became unreliable so I added another one and keep using both). The domains’ contents are supposed to be equivalent with the obvious difference of a different certificate location path and the domain name itself. I have two specific subdomains that don’t use mTLS and multiple others that do. This setup results in 4 blocks that each share a lot of the settings (of which 2 blocks share even more and the other 2 share even more too).
In my Caddyfile I have these 4 blocks in order of appearance:

  1. DOMAIN2, non-mTLS
  2. DOMAIN, non-mTLS
  3. DOMAIN2, mTLS
  4. DOMAIN, mTLS
    I handle the certificate renewal myself in other containers, the logs are rotated only in a single config instance as I’ve seen it recommended somewhere.
    Is there a way I can decrease all this repetition, or to streamline my configuration in any other way?

2. Error messages and/or full log output:

N/A

3. Caddy version:

v2.10.2

4. How I installed and ran Caddy:

a. System environment:

Fedora Server 43, x86_64, docker-ce v28.5.2, Docker Compose v2.40.3

b. Command:

docker compose up -d --build

```
$ cat caddy.dockerfile
FROM caddy:2-builder AS builder
RUN xcaddy build \
    --with github.com/hslatman/caddy-crowdsec-bouncer/http \
    --with github.com/porech/caddy-maxmind-geolocation

FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
```

c. Service/unit/compose file:

N/A

d. My complete Caddy config:

```
# global options block
{
    ## This sets up the endpoint, but does not do any actions
    crowdsec {
        api_key <API_KEY>
        api_url http://crowdsec:8080
    }
    # this ensures that the CrowdSec module is executed before any other HTTP handlers
    order crowdsec first
    # this only affects loggers not touched later (= only startup logs)
    log {
        format json {
            time_format iso8601
        }
    }
    auto_https disable_certs
}

(protected_endpoint) {
    forward_auth authelia:9091 {
        uri /api/authz/forward-auth
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
    }
}

(mutual_tls) {
    tls {
        protocols tls1.3
        client_auth {
            mode require_and_verify
            trust_pool file {
                pem_file /etc/caddy/ca.crt
            }
        }
    }
}

(log_stderr) {
    log {
        format json {
            time_format iso8601
        }
        output stderr
    }
}

(log_main_rotated) {
    log {
        format json {
            time_format iso8601
        }
        output file /logs/access.log {
            roll_size 30MiB
            roll_uncompressed
            roll_keep 3
        }
    }
}

(log_main_unrotated) {
    log {
        format json {
            time_format iso8601
        }
        output file /logs/access.log
    }
}

(geo_restrictions) {
  @denied {
    # block (NOT DE) and (NOT local) = allow either DE or local
    not maxmind_geolocation {
      db_path "/usr/share/GeoIP/GeoLite2-Country.mmdb"
      allow_countries DE
    }
    not remote_ip private_ranges
  }
  @deniedLOCAL {
    not remote_ip private_ranges
  }
}


www.{$DOMAIN2}, wiki.{$DOMAIN2} {
    # this is for crowdsec parsing...
    import log_main_rotated
    # ...and let's keep the stderr output for Portainer as well
    import log_stderr
    # only make this work for DE IPs (or local IPs)
    import geo_restrictions

    tls /etc/letsencrypt2/live/{$DOMAIN2}/fullchain.pem /etc/letsencrypt2/live/{$DOMAIN2}/privkey.pem

    # this does the actual crowdsec check
    crowdsec

    @wiki host wiki.{$DOMAIN2}
    handle @wiki {
        abort @denied
        reverse_proxy dokuwiki:80
    }

    @www host www.{$DOMAIN2}
    handle @www {
        abort @denied
        reverse_proxy php-apache:80
    }
}

www.{$DOMAIN}, wiki.{$DOMAIN} {
    # this is for crowdsec parsing...
    import log_main_unrotated
    # ...and let's keep the stderr output for Portainer as well
    import log_stderr
    # only make this work for DE IPs (or local IPs)
    import geo_restrictions

    tls /etc/letsencrypt/live/{$DOMAIN}/fullchain.pem /etc/letsencrypt/live/{$DOMAIN}/privkey.pem

    # this does the actual crowdsec check
    crowdsec

    @wiki host wiki.{$DOMAIN}
    handle @wiki {
        abort @denied
        #import protected_endpoint
        reverse_proxy dokuwiki:80
    }

    @www host www.{$DOMAIN}
    handle @www {
        abort @denied
        #import protected_endpoint
        reverse_proxy php-apache:80
    }
}

*.{$DOMAIN2} {
    import mutual_tls

    # this is for crowdsec parsing...
    import log_main_unrotated
    # ...and let's keep the stderr output for Portainer as well
    import log_stderr
    # only make this work for DE IPs (or local IPs)
    import geo_restrictions

    tls /etc/letsencrypt2/live/{$DOMAIN2}/fullchain.pem /etc/letsencrypt2/live/{$DOMAIN2}/privkey.pem

    # can we check here?
    crowdsec

    @auth host auth.{$DOMAIN2}
    handle @auth {
        abort @denied
        reverse_proxy authelia:9091
    }

    @dozzle host dozzle.{$DOMAIN2}
    handle @dozzle {
        abort @denied
        #abort @deniedLOCAL
        import protected_endpoint
        reverse_proxy dozzle:8080
    }

    # more
    # subdomains
    # here
}

*.{$DOMAIN} {
    import mutual_tls

    # this is for crowdsec parsing...
    import log_main_unrotated
    # ...and let's keep the stderr output for Portainer as well
    import log_stderr
    # only make this work for DE IPs (or local IPs)
    import geo_restrictions

    tls /etc/letsencrypt/live/{$DOMAIN}/fullchain.pem /etc/letsencrypt/live/{$DOMAIN}/privkey.pem

    # can we check here?
    crowdsec

    @auth host auth.{$DOMAIN}
    handle @auth {
        abort @denied
        reverse_proxy authelia:9091
    }

    @dozzle host dozzle.{$DOMAIN}
    handle @dozzle {
        abort @denied
        #abort @deniedLOCAL
        import protected_endpoint
        reverse_proxy dozzle:8080
    }

    # more
    # subdomains
    # here
}
```

5. Links to relevant resources:

N/A

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.