Caddy Onboarding LabChapter IV — From Command Line to JSON: Booting Caddy0/6Contents
Chapter IV

From Command Line to JSON: Booting Caddy

This module traces caddy run from the Cobra command tree in cmd/main.go down through cmdRun, showing how a config file — Caddyfile or otherwise — gets read, optionally adapted, and handed to caddy.Load as JSON. It shows that config adapters like the Caddyfile's ServerType.Setup are the real translators into Caddy's native JSON format, and that JSON, not Caddyfile, is what Caddy's runtime actually understands.


How `caddy run` gets dispatched
RegisterCommand / commands mapcmd/commands.go
defaultFactory (root *cobra.Command)cmd/cobra.go
'run' subcommandcmd/commands.go
'start' subcommandcmd/commands.go
'stop' subcommandcmd/commands.go
cmdRun()cmd/commandfuncs.go
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
walkthrough

cmdRun: from CLI invocation to a running config

cmd/commandfuncs.go · lines 188289
lines 188–189

cmdRun's very first act is trapping OS signals (SIGINT, SIGTERM, SIGUSR1 for reload, etc.), before any config is even read — so a slow or failing config load never leaves Caddy unable to shut down cleanly.

lines 205–211

The run command's flags are read here: --config/--adapter select what to load and how to interpret it, --resume prefers the autosave file over --config, --watch enables live reload, and --pidfile/--pingback support external process supervision.

lines 251–257

Unless --resume already produced a config, LoadConfig(configFlag, configAdapterFlag) does the real work: read the file (or stdin), and — if it isn't already JSON — run it through a config adapter before returning.

lines 259–267

The PID file is written now, before caddy.Load runs — a deliberate fix for issue #5477 — so external tooling can find the process even if adapting a large config takes a while.

lines 269–280

If a config file was actually used, SetLastConfig remembers it and stores a closure that knows how to reload and re-adapt it — this is what lets a running Caddy process reload from the same source file later, e.g. on SIGUSR1.

lines 282–289

Finally, the adapted (or already-JSON) bytes are handed to caddy.Load — the single entry point every config, from any source or format, funnels through to actually construct and start apps like the HTTP server explored in earlier modules.

func cmdRun(fl Flags) (int, error) {	caddy.TrapSignals()	// set up buffered logging for early startup	// so that we can hold onto logs until after	// the config is loaded (or fails to load)	// so that we can write the logs to the user's	// configured output. we must be sure to flush	// on any error before the config is loaded.	logger, defaultLogger, logBuffer := caddy.BufferedLog()	undoMaxProcs := setResourceLimits(logger)	defer undoMaxProcs()	// release the local reference to the undo function so it can be GC'd;	// the deferred call above has already captured the actual function value.	undoMaxProcs = nil //nolint:ineffassign,wastedassign	configFlag := fl.String("config")	configAdapterFlag := fl.String("adapter")	resumeFlag := fl.Bool("resume")	printEnvFlag := fl.Bool("environ")	watchFlag := fl.Bool("watch")	pidfileFlag := fl.String("pidfile")	pingbackFlag := fl.String("pingback")	// load all additional envs as soon as possible	err := handleEnvFileFlag(fl)	if err != nil {		logBuffer.FlushTo(defaultLogger)		return caddy.ExitCodeFailedStartup, err	}	// if we are supposed to print the environment, do that first	if printEnvFlag {		printEnvironment()	}	// load the config, depending on flags	var config []byte	if resumeFlag {		config, err = os.ReadFile(caddy.ConfigAutosavePath)		if errors.Is(err, fs.ErrNotExist) {			// not a bad error; just can't resume if autosave file doesn't exist			logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))			resumeFlag = false		} else if err != nil {			logBuffer.FlushTo(defaultLogger)			return caddy.ExitCodeFailedStartup, err		} else {			if configFlag == "" {				logger.Info("resuming from last configuration",					zap.String("autosave_file", caddy.ConfigAutosavePath))			} else {				// if they also specified a config file, user should be aware that we're not				// using it (doing so could lead to data/config loss by overwriting!)				logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",					zap.String("autosave_file", caddy.ConfigAutosavePath))			}		}	}	// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive	var configFile string	var adapterUsed string	if !resumeFlag {		config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)		if err != nil {			logBuffer.FlushTo(defaultLogger)			return caddy.ExitCodeFailedStartup, err		}	}	// create pidfile now, in case loading config takes a while (issue #5477)	if pidfileFlag != "" {		err := caddy.PIDFile(pidfileFlag)		if err != nil {			logger.Error("unable to write PID file",				zap.String("pidfile", pidfileFlag),				zap.Error(err))		}	}	// If we have a source config file (we're running via 'caddy run --config ...'),	// record it so SIGUSR1 can reload from the same file. Also provide a callback	// that knows how to load/adapt that source when requested by the main process.	if configFile != "" {		caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {			cfg, _, _, err := LoadConfig(file, adapter)			if err != nil {				return err			}			return caddy.Load(cfg, true)		})	}	// run the initial config	err = caddy.Load(config, true)	if err != nil {		logBuffer.FlushTo(defaultLogger)		return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)	}	// release the reference to the config so it can be GC'd	config = nil //nolint:ineffassign,wastedassign
step 1 of 6
	// load config adapter	if adapterName != "" {		cfgAdapter = caddyconfig.GetAdapter(adapterName)		if cfgAdapter == nil {			return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)		}	}	// adapt config	if cfgAdapter != nil {		adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{			"filename": configFile,		})		if err != nil {			return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)		}		logger.Info("adapted config to JSON", zap.String("adapter", adapterName))		for _, warn := range warnings {			msg := warn.Message			if warn.Directive != "" {				msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)			}			logger.Warn(msg,				zap.String("adapter", adapterName),				zap.String("file", warn.File),				zap.Int("line", warn.Line))		}		config = adaptedConfig
cmd/main.go · lines 202229
Figure II · This is the moment a Caddyfile (or any non-JSON format) stops being Caddy's problem and becomes JSON: cfgAdapter.Adapt runs whichever adapter was registered under adapterName — e.g. the 'caddyfile' adapter — and every byte that reaches caddy.Load from here on is that adapter's JSON output, never the original text.
Figure III · pipelineOne Caddyfile's trip into caddy.Load
6 stages
Press play — or step through the pipeline stage by stage.
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 2 passed

What actually determines whether `caddy run` sends a config file through a config adapter before loading it?

In cmdRun, why is the PID file written before caddy.Load runs, rather than after the server is confirmed up?