How Caddy's Process Lifecycle Works: Start, Stop, and Signal Handling

Caddy's process lifecycle is orchestrated through a deterministic sequence of configuration loading, context provisioning, and modular app management in caddy.go, supporting both graceful shutdowns via POSIX signals and programmatic control through the public Go API.

Caddy is a modern, extensible web server written in Go that manages its process lifecycle through a tightly controlled sequence of initialization, configuration loading, and modular app orchestration. Understanding how Caddy starts up and shuts down is essential for operators running production workloads and developers embedding Caddy into custom Go binaries. This article examines the core mechanisms defined in the caddyserver/caddy repository that govern process initialization, graceful termination, and signal handling.

The Boot Phase: CLI Entry Point

The lifecycle begins in cmd/main.go, which constructs the Cobra command tree and executes the selected subcommand. When you run caddy run, the binary enters the boot phase by parsing command-line flags and preparing the environment for configuration loading.

The entry point delegates to the run command implementation, which handles the transition from CLI arguments to the internal configuration system. This separation ensures that Caddy can be started equally from the command line, as a system service, or embedded as a library in another Go application.

The Start Phase: Configuration and App Initialization

Once the boot phase completes, Caddy enters the start phase through a chain of functions in caddy.go that transform raw configuration into running services.

From Command Line to Run()

The run command invokes caddy.Run(cfg), which marshals the provided Config struct to JSON and forwards it to caddy.Load. The Load function serves as the primary entry point for configuration changes, notifying the service manager and acquiring the global lock (rawCfgMu) to ensure thread safety.

Load delegates to changeConfig, which ultimately calls unsyncedDecodeAndRun. This function deserializes the JSON configuration and prepares the execution context.

Provisioning and Starting Apps

The provisionContext function in caddy.go creates a fresh Context instance that holds the active configuration, a cancellation function, and module-specific state. This context isolation allows Caddy to perform hot reloads by provisioning a new context while the old one remains active.

During provisioning, Caddy:

  1. Initializes storage modules
  2. Loads each App module defined in the configuration
  3. Invokes App.Start() on every loaded application

The final run function iterates over the apps map inside the context and starts each application. Only after all apps report successful startup does Caddy consider the start phase complete and begin accepting traffic.

The Stop Phase: Graceful Termination

Caddy supports multiple shutdown triggers, including the caddy stop command, API calls to caddy.Stop(), and POSIX signals. Regardless of the trigger, the stop phase follows a consistent path through caddy.go.

Signal Handling and Triggers

On POSIX platforms, sigtrap_posix.go installs a goroutine (trapSignalsPosix) that listens for SIGTERM, SIGQUIT, and SIGUSR1. SIGTERM triggers exitProcessFromSignal, which invokes exitProcess—the same graceful shutdown routine used by the administrative API. SIGQUIT bypasses graceful shutdown and immediately calls os.Exit(ExitCodeForceQuit).

SIGUSR1 initiates a hot reload by re-reading the last-known configuration file and invoking the reload callback, allowing zero-downtime configuration updates without restarting the process.

The Shutdown Sequence

When caddy.Stop() is called, it acquires the necessary locks and delegates to unsyncedStop. This function iterates over every started App in the current context and invokes App.Stop(), giving each module an opportunity to release resources, close connections, and persist state.

After all apps return from their Stop() methods, Caddy clears the global context (currentCtx), resets any persisted state such as the autosave file, and releases the configuration locks. This idempotent design ensures that repeated calls to Stop() remain safe.

Implementing the App Interface

Every Caddy component that participates in the lifecycle must implement the App interface defined in modules.go. This interface requires two methods:

  • Start() error – Called during the provisioning phase to initialize listeners, connect to databases, or begin background tasks
  • Stop() error – Called during shutdown to perform cleanup

The core orchestration logic in caddy.go remains agnostic to specific module implementations, interacting with all components solely through this interface. This abstraction enables the modular architecture where HTTP servers, TLS automation managers, and custom plugins all share the same lifecycle guarantees.

Practical Examples

Embedding Caddy Programmatically

You can embed Caddy directly into your own Go binary by constructing a configuration and calling caddy.Run():

package main

import (
	"context"
	"log"

	"github.com/caddyserver/caddy/v2"
)

func main() {
	// Build a minimal Config (e.g. a simple file server)
	cfg := &caddy.Config{
		AppsRaw: caddy.ModuleMap{
			"http": caddy.ModuleMap{
				"servers": caddy.ModuleMap{
					"srv0": caddy.ModuleMap{
						"listen": []string{":8080"},
						"routes": []any{
							caddy.ModuleMap{
								"handle": []any{
									caddy.ModuleMap{
										"handler": "static_response",
										"body":    "Hello from embedded Caddy!",
									},
								},
							},
						},
					},
				},
			},
		},
	}

	// Run the configuration (starts apps)
	if err := caddy.Run(cfg); err != nil {
		log.Fatalf("Caddy failed to start: %v", err)
	}
	// Block until the process receives a termination signal
	<-context.Background().Done()
}

Graceful Shutdown via API

To programmatically trigger a shutdown from within your application:

package main

import (
	"log"

	"github.com/caddyserver/caddy/v2"
)

func main() {
	// ... assume Caddy is already running ...

	if err := caddy.Stop(); err != nil {
		log.Printf("error stopping Caddy: %v", err)
	}
}

This invokes the same unsyncedStop routine used by POSIX signal handlers, ensuring consistent behavior whether stopped via code or system signals.

Summary

  • Boot phase: cmd/main.go initializes the CLI and delegates to the run subcommand, which transitions into the core lifecycle logic.
  • Start phase: caddy.Run() → Load() → provisionContext() creates isolated execution contexts and starts each App via App.Start().
  • Stop phase: caddy.Stop() → unsyncedStop() iterates all Apps and invokes App.Stop(), protected by rawCfgMu and currentCtxMu locks.
  • Signal handling: sigtrap_posix.go maps SIGTERM to graceful shutdown and SIGUSR1 to configuration reload, while SIGQUIT forces immediate termination.
  • App interface: Modular components implement Start() and Stop() methods, allowing the core to manage diverse functionality through a unified lifecycle contract.

Frequently Asked Questions

What signals does Caddy respond to for shutdown?

Caddy listens for SIGTERM and SIGQUIT on POSIX systems through the trapSignalsPosix function in sigtrap_posix.go. SIGTERM triggers a graceful shutdown via exitProcess, allowing apps to clean up resources, while SIGQUIT forces immediate termination with os.Exit(ExitCodeForceQuit). On non-POSIX platforms like Windows, sigtrap_nonposix.go provides stub implementations.

How does Caddy handle configuration reloads without downtime?

When Caddy receives SIGUSR1 or an API call to reload, it invokes Load with the new configuration while the existing context remains active. The provisionContext function creates a fresh Context with the updated config, starts the new apps, and only then swaps the global context pointer protected by currentCtxMu. This atomic hand-off ensures zero-downtime reloads.

Can I embed Caddy in my own Go application?

Yes, Caddy is designed as an embeddable library. Import github.com/caddyserver/caddy/v2, construct a caddy.Config struct (or load from JSON), and call caddy.Run(cfg). To stop the embedded server, call caddy.Stop(). This API bypasses the CLI entirely, giving you programmatic control over the process lifecycle within your own binary.

What is the difference between graceful and forced shutdown?

Graceful shutdown, triggered by caddy.Stop() or SIGTERM, executes unsyncedStop which calls App.Stop() on every running module, allowing proper cleanup. Forced shutdown via SIGQUIT or caddy stop --force skips the app cleanup loop and exits immediately. The forced path does not invoke unsyncedStop, making it suitable for situations where the process is unresponsive but potentially risky for data consistency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →