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"))