caddyserver/caddy · critical

caddy process exited with error: %v

Error message

caddy process exited with error: %v

What it means

Returned by `caddy start` when the spawned child process exits before dialing back the success listener. The parent selects between the pingback channel and cmd.Wait(); if the child dies first (bad config, invalid flags, missing permissions), its exit error is surfaced here. This is the primary 'caddy start failed' signal and almost always means the config or flags are invalid.

Source

Thrown at cmd/commandfuncs.go:166

				break
			}
			log.Println(err)
		}
	}()

	// in another goroutine, we await the failure of the child process
	go func() {
		err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
		exit <- err       // sending on separate line ensures select won't trigger until after Wait unblocks
	}()

	// when one of the goroutines unblocks, we're done and can exit
	select {
	case <-success:
		fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid)
	case err := <-exit:
		return caddy.ExitCodeFailedStartup,
			fmt.Errorf("caddy process exited with error: %v", err)
	}

	return caddy.ExitCodeSuccess, nil
}

type tcpListenFunc func(network, address string) (net.Listener, error)

func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
	ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
	if ipv4Err == nil {
		return ln, nil
	}

	ln, ipv6Err := listen("tcp6", "[::1]:0")
	if ipv6Err == nil {
		return ln, nil
	}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Run `caddy run --config <file> --adapter <name>` in the foreground — it shows the child's real error output directly
  2. Check the child's stderr that `caddy start` already passes through (cmd.Stderr = os.Stderr)
  3. Verify ports: ss -ltnp | grep -E ':80|:443' and free them or adjust the config
  4. caddy validate --config <file> to catch config errors before starting

Example fix

# before
caddy start --config Caddyfile  # caddy process exited with error: ... listen tcp :80: bind: permission denied
# after
sudo setcap 'cap_net_bind_service=+ep' $(which caddy) && caddy start --config Caddyfile
Defensive patterns

Strategy: validation

Validate before calling

// catch config/port problems BEFORE caddy start
if out, err := exec.Command("caddy", "validate", "--config", cfg).CombinedOutput(); err != nil {
    return fmt.Errorf("config invalid, refusing to start: %s", out)
}
for _, p := range []string{":80", ":443"} {
    if ln, err := net.Listen("tcp", p); err != nil {
        return fmt.Errorf("port %s unavailable: %w", p, err)
    } else { ln.Close() }
}

Try / catch

_, err := caddycmd.Start(fl)
if err != nil && strings.Contains(err.Error(), "caddy process exited with error") {
    // child's stderr was inherited and printed above it — read that output for root cause
    // then fix config/flags and retry once; repeated retries without changes will keep failing
}

Prevention

When it happens

Trigger: `caddy start --config bad-Caddyfile` where the child fails to load config; passing an adapter name not compiled in; child cannot bind the configured ports (permission denied on :80/:443 without privileges, or ports in use); invalid admin endpoint address.

Common situations: Starting Caddy on ports <1024 as non-root; another process already on :80/:443; typos in the Caddyfile; --adapter jsonfile unavailable in a custom build; systemd unit passing conflicting flags.

Related errors


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