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

  1. Check that every env var referenced by {env.*} in listen addresses is set for the Caddy process (print env inside the same unit)
  2. Fix placeholder syntax: balanced braces, correct namespace (env., service., etc.)
  3. 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

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


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/e3c78c13ff2e22a1. Report an issue: GitHub.