caddyserver/caddy · error
server %s, listener %d: %v
Error message
server %s, listener %d: %v
What it means
Caddy expands {placeholders} in each servers.<name>.listen address during provisioning (repl.ReplaceOrErr with errOnEmpty/errOnUnknown). If a placeholder is unknown or resolves to an empty string, provisioning of that server fails, naming the server and the listener index. Only the first bad listener is reported.
Source
Thrown at modules/caddyhttp/app.go:317
// set the default client IP header to read from
if srv.ClientIPHeaders == nil {
srv.ClientIPHeaders = []string{"X-Forwarded-For"}
}
// precompute underscore and dot header allowlist rules
if err := srv.provisionUnderscoreHeaders(); err != nil {
return fmt.Errorf("server %s: %v", srvName, err)
}
if err := srv.provisionDotHeaders(); err != nil {
return fmt.Errorf("server %s: %v", srvName, err)
}
// process each listener address
for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v", srvName, i, err)
}
srv.Listen[i] = lnOut
}
// set up each listener modifier
if srv.ListenerWrappersRaw != nil {
vals, err := ctx.LoadModule(srv, "ListenerWrappersRaw")
if err != nil {
return fmt.Errorf("loading listener wrapper modules: %v", err)
}
var hasTLSPlaceholder bool
for i, val := range vals.([]any) {
if _, ok := val.(*tlsPlaceholderWrapper); ok {
if i == 0 {
// putting the tls placeholder wrapper first is nonsensical because
// that is the default, implicit setting: without it, all wrappers
// will go after the TLS listener anyway
return fmt.Errorf("it is unnecessary to specify the TLS listener wrapper in the first position because that is the default")View on GitHub (pinned to 50e54ee279)
Solutions
- Check that every env var referenced by {env.*} in listen addresses is set for the Caddy process (print env inside the same unit)
- Fix placeholder syntax: balanced braces, correct namespace (env., service., etc.)
- Give the placeholder a safe literal default instead of relying on it being set
Example fix
// before
"listen": ["{env.BIND_ADDR}:443"] // BIND_ADDR unset
// after
"listen": ["{$BIND_ADDR:localhost}:443"] // with default Defensive patterns
Strategy: validation
Validate before calling
// fail fast before caddy.Load if env placeholders are used in listen
for _, l := range srvCfg.Listen {
if strings.Contains(l, "{env.") {
for _, m := range regexp.MustCompile(`\{env\.([A-Za-z0-9_]+)\}`).FindAllStringSubmatch(l, -1) {
if os.Getenv(m[1]) == "" {
return fmt.Errorf("env %s needed by listen %q is unset", m[1], l)
}
}
}
} Try / catch
if err := caddy.Load(cfgJSON, true); err != nil { /* message includes server+listener index; fix placeholder source */ } Prevention
- Prefer Caddyfile `{$VAR:default}` env syntax with defaults over JSON {env.*} in listen
- Export all env vars in the systemd unit / container before ExecStart
- Use `caddy adapt --pretty` to inspect what placeholders remain unresolved
When it happens
Trigger: A listen address containing an unset environment placeholder like "{env.LISTEN_ADDR}", a malformed placeholder such as "{env.LISTEN_ADDR" (missing brace), or a placeholder whose value is empty at load time.
Common situations: Deploying a templated config where LISTEN_ADDR/HOST/PORT env vars are not exported in the service unit; typos in placeholder names; systemd/Docker env not passed to the Caddy process.
Related errors
- %s: parsing listen address '%s': %v
- replacing listen address: %v
- evaluated placeholder %s%s%s is empty
- --input is required
- loading new config: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/e3c78c13ff2e22a1.
Report an issue: GitHub.