Caddy’s hot-reloading capability means you can update its configuration without interrupting any in-flight requests.
Here’s Caddy serving a simple HTTP site:
{
"apps": {
"http": {
"servers": {
"my_server": {
"listen": [":80"],
"routes": [
{
"handle": [
{
"handler": "static_file_server",
"root": "/var/www/html"
}
]
}
]
}
}
}
}
}
Now, let’s say you want to change the root directory to /var/www/new_html. Instead of stopping and starting Caddy, you can simply send a SIGHUP signal to the Caddy process.
kill -HUP $(pidof caddy)
This command finds the process ID (PID) of Caddy using pidof and then sends it the HUP (Hang Up) signal. Caddy, upon receiving SIGHUP, will re-read its configuration file (typically Caddyfile or caddy.json if specified) and apply the changes.
The magic of graceful reload lies in how Caddy manages its worker processes and listener sockets. When SIGHUP is received, Caddy doesn’t immediately kill its old worker processes. Instead, it starts new worker processes with the updated configuration and gradually drains connections from the old workers. Any new incoming connections are immediately handled by the new workers. Existing connections on the old workers are allowed to complete their current request, and then the old workers are shut down. This ensures that no request is dropped mid-flight.
The primary mechanism for this is Caddy’s internal HTTP server. It maintains a list of active connections. When a reload is triggered, it forks new worker processes. The parent process then tells the old workers to stop accepting new connections, and it starts accepting new connections on the new workers. The old workers continue to serve existing requests until they are finished. This is fundamentally different from a hard restart, where all connections are abruptly terminated.
The key to Caddy’s graceful shutdown is its ability to manage listener sockets. When a reload occurs, Caddy can reuse the existing listener sockets that are bound to the ports. This means it doesn’t need to re-bind to the ports, which could otherwise cause a brief interruption. The new worker processes take over the existing sockets, allowing them to immediately start accepting new connections without any downtime.
You can verify that the reload was successful by checking Caddy’s logs. If you’re running Caddy with a JSON configuration, you might see log entries indicating that the configuration has been reloaded. For example, you might see something like:
{"level":"info","ts":"2023-10-27T10:00:00Z","msg":"serving configuration","config":"...","error":""}
And after the reload:
{"level":"info","ts":"2023-10-27T10:05:00Z","msg":"serving configuration","config":"...","error":""}
The content of the config field would reflect the updated configuration.
Another way to trigger a reload, especially if Caddy is managed by systemd, is by using systemctl reload caddy. This command is a wrapper that sends the appropriate signal to the Caddy process managed by systemd.
If you’re using Docker, you can achieve the same by sending the SIGHUP signal to the Caddy container:
docker exec <caddy_container_name_or_id> kill -HUP 1
Here, 1 is typically the PID of the Caddy process inside the container.
The most surprising thing about Caddy’s hot-reloading is that it doesn’t rely on complex state synchronization or external coordination mechanisms. It’s a direct implementation of the SIGHUP signal pattern, adapted for its own internal architecture of worker processes and shared listeners. This simplicity makes it robust and efficient.
The one thing most people don’t know is that Caddy also supports reloading via its Admin API. If you have the Admin API enabled, you can POST the new configuration to the /config endpoint. This is often more programmatic and useful in automated deployment scenarios than sending signals directly. You’d typically use curl for this:
curl localhost:2019/config \
--header "Content-Type: application/json" \
--data @caddy.json
This sends the contents of caddy.json to the Admin API, triggering a configuration reload.
Understanding how Caddy manages its worker lifecycle during a reload is key to appreciating its zero-downtime philosophy.