caddyserver/caddy · error
automation policy %d: cannot apply more than one automation
Error message
automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)
What it means
Policy matching is first-match-wins, so a host may only appear as a subject in one automation policy; a second policy listing the same host is silently ineffective. Validation builds a host->policy-index map and rejects duplicates, naming both the offending policy index and the earlier policy that already claimed the host.
Source
Thrown at modules/caddytls/tls.go:386
// Validate validates t's configuration.
func (t *TLS) Validate() error {
if t.Automation != nil {
// ensure that host aren't repeated; since only the first
// automation policy is used, repeating a host in the lists
// isn't useful and is probably a mistake; same for two
// catch-all/default policies
var hasDefault bool
hostSet := make(map[string]int)
for i, ap := range t.Automation.Policies {
if len(ap.subjects) == 0 {
if hasDefault {
return fmt.Errorf("automation policy %d is the second policy that acts as default/catch-all, but will never be used", i)
}
hasDefault = true
}
for _, h := range ap.subjects {
if first, ok := hostSet[h]; ok {
return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first)
}
hostSet[h] = i
}
}
}
if t.Cache != nil {
if t.Cache.Capacity < 0 {
return fmt.Errorf("cache capacity must be >= 0")
}
}
return nil
}
// Start activates the TLS module.
func (t *TLS) Start() error {
// warn if on-demand TLS is enabled but no restrictions are in place
if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.permission == nil) {
for _, ap := range t.Automation.Policies {View on GitHub (pinned to 50e54ee279)
Solutions
- Keep each subject in exactly one policy; consolidate the settings for the duplicate host into the first policy that should own it
- If different issuers are intended per-subdomain, split subjects precisely (e.g. move only www.example.com to the second policy)
- Use 'caddy adapt' + inspect the JSON to see the final policy subjects after snippet imports
Example fix
// before
"policies": [
{"subjects": ["example.com"], "issuers": [{"module": "acme"}]},
{"subjects": ["example.com", "api.example.com"], "issuers": [{"module": "internal"}]}
]
// after
"policies": [
{"subjects": ["example.com"], "issuers": [{"module": "acme"}]},
{"subjects": ["api.example.com"], "issuers": [{"module": "internal"}]}
] Defensive patterns
Strategy: validation
Validate before calling
seen := map[string]int{}
for i, p := range tlsApp.Automation.Policies {
for _, h := range p.SubjectsRaw {
if first, dup := seen[h]; dup {
return fmt.Errorf("host %s in policies %d and %d", h, first, i)
}
seen[h] = i
}
} Prevention
- Keep one canonical policy per hostname; deduplicate after merging snippets
- Inspect adapted JSON ('caddy adapt --pretty') to see effective subject lists
When it happens
Trigger: automation.policies = [{subjects: ["example.com"], ...}, {subjects: ["example.com", "www.example.com"], ...}] — example.com is claimed by policy 0, so policy 1 triggers the error.
Common situations: Splitting config snippets that each manage overlapping domains; merging site configs into policies without deduplicating; wildcard policies that also explicitly list a covered name in another policy.
Related errors
- automation policy %d is the second policy that acts as defau
- invalid TLS renegotiation level: %v
- not a valid big integer: %s
- unsupported cipher suite: %s
- protocol min (%x) cannot be greater than protocol max (%x)
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/506015bc07428e3e.
Report an issue: GitHub.