caddyserver/caddy · error

expanding email address '%s': %v

Error message

expanding email address '%s': %v

What it means

Returned by ACMEIssuer.Provision (modules/caddytls/acmeissuer.go:138) when the configured email address contains a placeholder and the Caddy replacer fails to expand it. ReplaceOrErr is called with errorOnUnset=true, so an unknown/unresolvable placeholder or a malformed placeholder expression is an error rather than an empty string. Fails at config provisioning, before any ACME traffic.

Source

Thrown at modules/caddytls/acmeissuer.go:138

// CaddyModule returns the Caddy module information.
func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "tls.issuance.acme",
		New: func() caddy.Module { return new(ACMEIssuer) },
	}
}

// Provision sets up iss.
func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
	iss.logger = ctx.Logger()

	repl := caddy.NewReplacer()

	// expand email address, if non-empty
	if iss.Email != "" {
		email, err := repl.ReplaceOrErr(iss.Email, true, true)
		if err != nil {
			return fmt.Errorf("expanding email address '%s': %v", iss.Email, err)
		}
		iss.Email = email
	}

	// expand CA endpoint, if non-empty
	if iss.CA != "" {
		ca, err := repl.ReplaceOrErr(iss.CA, true, true)
		if err != nil {
			return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err)
		}
		iss.CA = ca
	}

	// expand TestCA endpoint, if non-empty
	if iss.TestCA != "" {
		testca, err := repl.ReplaceOrErr(iss.TestCA, true, true)
		if err != nil {
			return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err)

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Set the missing variable in the environment Caddy actually runs in (systemctl edit caddy -> Environment=..., or the container env) and restart
  2. Fix the placeholder syntax: exactly {env.VAR_NAME} with matching braces
  3. If the value is not secret-dependent, hardcode the email or use the ACME_EMAIL/admin email defaults and drop the placeholder
  4. Test with caddy adapt + caddy validate --adapter caddyfile to catch it before reload

Example fix

# before
 example.com {
   tls {
     email {env.ACME_MAIL}   # typo: variable is ACME_EMAIL
   }
 }

# after
 example.com {
   tls {
     email {env.ACME_EMAIL}
   }
 }
# and: systemctl edit caddy -> [Service] Environment=ACME_EMAIL=me@example.com
Defensive patterns

Strategy: validation

Validate before calling

// expose placeholder problems before Caddy loads the config
import "os"

func envPlaceholdersSet(cfg string) error {
	re := regexp.MustCompile(`\{env\.([A-Za-z0-9_]+)}`)
	for _, m := range re.FindAllStringSubmatch(cfg, -1) {
		if os.Getenv(m[1]) == "" {
			return fmt.Errorf("placeholder {env.%s} is not set", m[1])
		}
	}
	return nil
}

Try / catch

if err := issuer.Provision(ctx); err != nil {
    if strings.Contains(err.Error(), "expanding email address") {
        // unset env var or bad placeholder: fix environment, reload config
    }
    return err
}

Prevention

When it happens

Trigger: Setting email to a placeholder like {env.ACME_EMAIL} in the tls/ACME issuer config when the environment variable is not set in Caddy's process, or using placeholder syntax the replacer cannot resolve (typo, unsupported key, unclosed braces).

Common situations: Env var name mismatch (ACME_EMAIL vs CERT_EMAIL); .env files loaded by a wrapper but not by the systemd unit running Caddy; container env vars set in one service but not the one running Caddy; typos like {env.EMAIL}}.

Related errors


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