@marcel91200 / caddy Mirror · read-only / commits / 65e0ddc2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 21:00 UTC

core: Reloading with `SIGUSR1` if config never changed via admin (#7258)

Francis Lavoie committed Sep 26, 2025 at 12:50 UTC 65e0ddc22137bbbaa68c842ae0b98d0548504545
7 files changed +167 -20
admin.go
+7
@@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
1029 return err
1030 }
1031
1032 + // If this request changed the config, clear the last
1033 + // config info we have stored, if it is different from
1034 + // the original source.
1035 + ClearLastConfigIfDifferent(
1036 + r.Header.Get("Caddy-Config-Source-File"),
1037 + r.Header.Get("Caddy-Config-Source-Adapter"))
1038 +
1039 default:
1040 return APIError{
1041 HTTPStatus: http.StatusMethodNotAllowed,
caddy.go
+85
@@ -1197,6 +1197,91 @@ var (
1197 rawCfgMu sync.RWMutex
1198 )
1199
1200 +// lastConfigFile and lastConfigAdapter remember the source config
1201 +// file and adapter used when Caddy was started via the CLI "run" command.
1202 +// These are consulted by the SIGUSR1 handler to attempt reloading from
1203 +// the same source. They are intentionally not set for other entrypoints
1204 +// such as "caddy start" or subcommands like file-server.
1205 +var (
1206 + lastConfigMu sync.RWMutex
1207 + lastConfigFile string
1208 + lastConfigAdapter string
1209 +)
1210 +
1211 +// reloadFromSourceFunc is the type of stored callback
1212 +// which is called when we receive a SIGUSR1 signal.
1213 +type reloadFromSourceFunc func(file, adapter string) error
1214 +
1215 +// reloadFromSourceCallback is the stored callback
1216 +// which is called when we receive a SIGUSR1 signal.
1217 +var reloadFromSourceCallback reloadFromSourceFunc
1218 +
1219 +// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
1220 +var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
1221 +
1222 +// SetLastConfig records the given source file and adapter as the
1223 +// last-known external configuration source. Intended to be called
1224 +// only when starting via "caddy run --config <file> --adapter <adapter>".
1225 +func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
1226 + lastConfigMu.Lock()
1227 + lastConfigFile = file
1228 + lastConfigAdapter = adapter
1229 + reloadFromSourceCallback = fn
1230 + lastConfigMu.Unlock()
1231 +}
1232 +
1233 +// ClearLastConfigIfDifferent clears the recorded last-config if the provided
1234 +// source file/adapter do not match the recorded last-config. If both srcFile
1235 +// and srcAdapter are empty, the last-config is cleared.
1236 +func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
1237 + if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
1238 + return
1239 + }
1240 + SetLastConfig("", "", nil)
1241 +}
1242 +
1243 +// getLastConfig returns the last-known config file and adapter.
1244 +func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
1245 + lastConfigMu.RLock()
1246 + f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
1247 + lastConfigMu.RUnlock()
1248 + return f, a, cb
1249 +}
1250 +
1251 +// lastConfigMatches returns true if the provided source file and/or adapter
1252 +// matches the recorded last-config. Matching rules (in priority order):
1253 +// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
1254 +// 2. If srcFile exactly equals the recorded file, match.
1255 +// 3. If both sides can be made absolute and equal, match.
1256 +// 4. If basenames are equal, match.
1257 +func lastConfigMatches(srcFile, srcAdapter string) bool {
1258 + lf, la, _ := getLastConfig()
1259 +
1260 + // If adapter is provided, it must match.
1261 + if srcAdapter != "" && srcAdapter != la {
1262 + return false
1263 + }
1264 +
1265 + // Quick equality check.
1266 + if srcFile == lf {
1267 + return true
1268 + }
1269 +
1270 + // Try absolute path comparison.
1271 + sAbs, sErr := filepath.Abs(srcFile)
1272 + lAbs, lErr := filepath.Abs(lf)
1273 + if sErr == nil && lErr == nil && sAbs == lAbs {
1274 + return true
1275 + }
1276 +
1277 + // Final fallback: basename equality.
1278 + if filepath.Base(srcFile) == filepath.Base(lf) {
1279 + return true
1280 + }
1281 +
1282 + return false
1283 +}
1284 +
1285 // errSameConfig is returned if the new config is the same
1286 // as the old one. This isn't usually an actual, actionable
1287 // error; it's mostly a sentinel value.
caddyconfig/load.go
+7
@@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
121 }
122 }
123
124 + // If this request changed the config, clear the last
125 + // config info we have stored, if it is different from
126 + // the original source.
127 + caddy.ClearLastConfigIfDifferent(
128 + r.Header.Get("Caddy-Config-Source-File"),
129 + r.Header.Get("Caddy-Config-Source-Adapter"))
130 +
131 caddy.Log().Named("admin.api").Info("load complete")
132
133 return nil
cmd/commandfuncs.go
+23 -5
@@ -231,8 +231,9 @@ func cmdRun(fl Flags) (int, error) {
231 }
232 // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
233 var configFile string
234 + var adapterUsed string
235 if !resumeFlag {
235 - config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
236 + config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
237 if err != nil {
238 logBuffer.FlushTo(defaultLogger)
239 return caddy.ExitCodeFailedStartup, err
@@ -249,6 +250,19 @@ func cmdRun(fl Flags) (int, error) {
250 }
251 }
252
253 + // If we have a source config file (we're running via 'caddy run --config ...'),
254 + // record it so SIGUSR1 can reload from the same file. Also provide a callback
255 + // that knows how to load/adapt that source when requested by the main process.
256 + if configFile != "" {
257 + caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
258 + cfg, _, _, err := LoadConfig(file, adapter)
259 + if err != nil {
260 + return err
261 + }
262 + return caddy.Load(cfg, true)
263 + })
264 + }
265 +
266 // run the initial config
267 err = caddy.Load(config, true)
268 if err != nil {
@@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) {
309 // if enabled, reload config file automatically on changes
310 // (this better only be used in dev!)
311 if watchFlag {
298 - go watchConfigFile(configFile, configAdapterFlag)
312 + go watchConfigFile(configFile, adapterUsed)
313 }
314
315 // warn if the environment does not provide enough information about the disk
@@ -350,7 +364,7 @@ func cmdReload(fl Flags) (int, error) {
364 forceFlag := fl.Bool("force")
365
366 // get the config in caddy's native format
353 - config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
367 + config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
368 if err != nil {
369 return caddy.ExitCodeFailedStartup, err
370 }
@@ -368,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
382 if forceFlag {
383 headers.Set("Cache-Control", "must-revalidate")
384 }
385 + // Provide the source file/adapter to the running process so it can
386 + // preserve its last-config knowledge if this reload came from the same source.
387 + headers.Set("Caddy-Config-Source-File", configFile)
388 + headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
389
390 resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
391 if err != nil {
@@ -582,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
600 fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
601 }
602
585 - input, _, err := LoadConfig(configFlag, adapterFlag)
603 + input, _, _, err := LoadConfig(configFlag, adapterFlag)
604 if err != nil {
605 return caddy.ExitCodeFailedStartup, err
606 }
@@ -797,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
815 loadedConfig := config
816 if len(loadedConfig) == 0 {
817 // get the config in caddy's native format
800 - loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
818 + loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
819 if err != nil {
820 return "", err
821 }
cmd/main.go
+18 -13
@@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
100 // there is no config available. It prints any warnings to stderr,
101 // and returns the resulting JSON config bytes along with
102 // the name of the loaded config file (if any).
103 -func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
103 +// The return values are:
104 +// - config bytes (nil if no config)
105 +// - config file used ("" if none)
106 +// - adapter used ("" if none)
107 +// - error, if any
108 +func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
109 return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
110 }
111
@@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
143 return false, nil
144 }
145
141 -func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
146 +func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
147 // if no logger is provided, use a nop logger
148 // just so we don't have to check for nil
149 if logger == nil {
@@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
152
153 // specifying an adapter without a config file is ambiguous
154 if adapterName != "" && configFile == "" {
150 - return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
155 + return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
156 }
157
158 // load initial config and adapter
@@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
163 if configFile == "-" {
164 config, err = io.ReadAll(os.Stdin)
165 if err != nil {
161 - return nil, "", fmt.Errorf("reading config from stdin: %v", err)
166 + return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
167 }
168 logger.Info("using config from stdin")
169 } else {
170 config, err = os.ReadFile(configFile)
171 if err != nil {
167 - return nil, "", fmt.Errorf("reading config from file: %v", err)
172 + return nil, "", "", fmt.Errorf("reading config from file: %v", err)
173 }
174 logger.Info("using config from file", zap.String("file", configFile))
175 }
@@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
184 cfgAdapter = nil
185 } else if err != nil {
186 // default Caddyfile exists, but error reading it
182 - return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
187 + return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
188 } else {
189 // success reading default Caddyfile
190 configFile = "Caddyfile"
@@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
196 if yes, err := isCaddyfile(configFile, adapterName); yes {
197 adapterName = "caddyfile"
198 } else if err != nil {
194 - return nil, "", err
199 + return nil, "", "", err
200 }
201
202 // load config adapter
203 if adapterName != "" {
204 cfgAdapter = caddyconfig.GetAdapter(adapterName)
205 if cfgAdapter == nil {
201 - return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
206 + return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
207 }
208 }
209
@@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
213 "filename": configFile,
214 })
215 if err != nil {
211 - return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
216 + return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
217 }
218 logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
219 for _, warn := range warnings {
@@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
231 // validate that the config is at least valid JSON
232 err = json.Unmarshal(config, new(any))
233 if err != nil {
229 - return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
234 + return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
235 }
236 }
237
233 - return config, configFile, nil
238 + return config, configFile, adapterName, nil
239 }
240
241 // watchConfigFile watches the config file at filename for changes
@@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
261 }
262
263 // get current config
259 - lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
264 + lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
265 if err != nil {
266 logger().Error("unable to load latest config", zap.Error(err))
267 return
@@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
273 //nolint:staticcheck
274 for range time.Tick(1 * time.Second) {
275 // get current config
271 - newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
276 + newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
277 if err != nil {
278 logger().Error("unable to load latest config", zap.Error(err))
279 return
cmd/storagefuncs.go
+1 -1
@@ -36,7 +36,7 @@ type storVal struct {
36 // determineStorage returns the top-level storage module from the given config.
37 // It may return nil even if no error.
38 func determineStorage(configFile string, configAdapter string) (*storVal, error) {
39 - cfg, _, err := LoadConfig(configFile, configAdapter)
39 + cfg, _, _, err := LoadConfig(configFile, configAdapter)
40 if err != nil {
41 return nil, err
42 }
sigtrap_posix.go
+26 -1
@@ -18,6 +18,7 @@ package caddy
18
19 import (
20 "context"
21 + "errors"
22 "os"
23 "os/signal"
24 "syscall"
@@ -48,7 +49,31 @@ func trapSignalsPosix() {
49 exitProcessFromSignal("SIGTERM")
50
51 case syscall.SIGUSR1:
51 - Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
52 + logger := Log().With(zap.String("signal", "SIGUSR1"))
53 + // If we know the last source config file/adapter (set when starting
54 + // via `caddy run --config <file> --adapter <adapter>`), attempt
55 + // to reload from that source. Otherwise, ignore the signal.
56 + file, adapter, reloadCallback := getLastConfig()
57 + if file == "" {
58 + logger.Info("last config unknown, ignored SIGUSR1")
59 + break
60 + }
61 + logger = logger.With(
62 + zap.String("file", file),
63 + zap.String("adapter", adapter))
64 + if reloadCallback == nil {
65 + logger.Warn("no reload helper available, ignored SIGUSR1")
66 + break
67 + }
68 + logger.Info("reloading config from last-known source")
69 + if err := reloadCallback(file, adapter); errors.Is(err, errReloadFromSourceUnavailable) {
70 + // No reload helper available (likely not started via caddy run).
71 + logger.Warn("reload from source unavailable in this process; ignored SIGUSR1")
72 + } else if err != nil {
73 + logger.Error("failed to reload config from file", zap.Error(err))
74 + } else {
75 + logger.Info("successfully reloaded config from file")
76 + }
77
78 case syscall.SIGUSR2:
79 Log().Info("not implemented", zap.String("signal", "SIGUSR2"))