diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index 51411a9a8..710532047 100644
--- a/caddyconfig/httpcaddyfile/addresses.go
+++ b/caddyconfig/httpcaddyfile/addresses.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"net"
 	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 	"unicode"
@@ -163,6 +164,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
 
 		sbaddrs = append(sbaddrs, a)
 	}
+
+	// sort them by their first address (we know there will always be at least one)
+	// to avoid problems with non-deterministic ordering (makes tests flaky)
+	sort.Slice(sbaddrs, func(i, j int) bool {
+		return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
+	})
+
 	return sbaddrs
 }
 
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 35eab9069..e4e40b282 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -218,13 +218,6 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
 		return nil, warnings, err
 	}
 
-	// if experimental HTTP/3 is enabled, enable it on each server
-	if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
-		for _, srv := range httpApp.Servers {
-			srv.ExperimentalHTTP3 = true
-		}
-	}
-
 	// extract any custom logs, and enforce configured levels
 	var customLogs []namedCustomLog
 	var hasDefaultLog bool
@@ -311,23 +304,54 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
 	}
 
 	for _, segment := range serverBlocks[0].block.Segments {
-		dir := segment.Directive()
+		opt := segment.Directive()
 		var val interface{}
 		var err error
 		disp := caddyfile.NewDispenser(segment)
 
-		dirFunc, ok := registeredGlobalOptions[dir]
+		optFunc, ok := registeredGlobalOptions[opt]
 		if !ok {
 			tkn := segment[0]
-			return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, dir)
+			return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
 		}
 
-		val, err = dirFunc(disp)
+		val, err = optFunc(disp)
 		if err != nil {
-			return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
+			return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
 		}
 
-		options[dir] = val
+		// As a special case, fold multiple "servers" options together
+		// in an array instead of overwriting a possible existing value
+		if opt == "servers" {
+			existingOpts, ok := options[opt].([]serverOptions)
+			if !ok {
+				existingOpts = []serverOptions{}
+			}
+			serverOpts, ok := val.(serverOptions)
+			if !ok {
+				return nil, fmt.Errorf("unexpected type from 'servers' global options")
+			}
+			options[opt] = append(existingOpts, serverOpts)
+			continue
+		}
+
+		options[opt] = val
+	}
+
+	// If we got "servers" options, we'll sort them by their listener address
+	if serverOpts, ok := options["servers"].([]serverOptions); ok {
+		sort.Slice(serverOpts, func(i, j int) bool {
+			return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
+		})
+
+		// Reject the config if there are duplicate listener address
+		seen := make(map[string]bool)
+		for _, entry := range serverOpts {
+			if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
+				return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
+			}
+			seen[entry.ListenerAddress] = true
+		}
 	}
 
 	return serverBlocks[1:], nil
@@ -602,6 +626,11 @@ func (st *ServerType) serversFromPairings(
 		servers[fmt.Sprintf("srv%d", i)] = srv
 	}
 
+	err := applyServerOptions(servers, options, warnings)
+	if err != nil {
+		return nil, err
+	}
+
 	return servers, nil
 }
 
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 7d34805cd..5001974b6 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -43,6 +43,7 @@ func init() {
 	RegisterGlobalOption("local_certs", parseOptTrue)
 	RegisterGlobalOption("key_type", parseOptSingleString)
 	RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
+	RegisterGlobalOption("servers", parseServerOptions)
 }
 
 func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
@@ -361,3 +362,7 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
 	}
 	return val, nil
 }
+
+func parseServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
+	return unmarshalCaddyfileServerOptions(d)
+}
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
new file mode 100644
index 000000000..38fa0f155
--- /dev/null
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -0,0 +1,235 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package httpcaddyfile
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
+	"github.com/dustin/go-humanize"
+)
+
+// serverOptions collects server config overrides parsed from Caddyfile global options
+type serverOptions struct {
+	// If set, will only apply these options to servers that contain a
+	// listener address that matches exactly. If empty, will apply to all
+	// servers that were not already matched by another serverOptions.
+	ListenerAddress string
+
+	// These will all map 1:1 to the caddyhttp.Server struct
+	ListenerWrappersRaw []json.RawMessage
+	ReadTimeout         caddy.Duration
+	ReadHeaderTimeout   caddy.Duration
+	WriteTimeout        caddy.Duration
+	IdleTimeout         caddy.Duration
+	MaxHeaderBytes      int
+	AllowH2C            bool
+	ExperimentalHTTP3   bool
+	StrictSNIHost       *bool
+}
+
+func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
+	serverOpts := serverOptions{}
+	for d.Next() {
+		if d.NextArg() {
+			serverOpts.ListenerAddress = d.Val()
+			if d.NextArg() {
+				return nil, d.ArgErr()
+			}
+		}
+		for nesting := d.Nesting(); d.NextBlock(nesting); {
+			switch d.Val() {
+			case "listener_wrappers":
+				for nesting := d.Nesting(); d.NextBlock(nesting); {
+					mod, err := caddy.GetModule("caddy.listeners." + d.Val())
+					if err != nil {
+						return nil, fmt.Errorf("finding listener module '%s': %v", d.Val(), err)
+					}
+					unm, ok := mod.New().(caddyfile.Unmarshaler)
+					if !ok {
+						return nil, fmt.Errorf("listener module '%s' is not a Caddyfile unmarshaler", mod)
+					}
+					err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+					if err != nil {
+						return nil, err
+					}
+					listenerWrapper, ok := unm.(caddy.ListenerWrapper)
+					if !ok {
+						return nil, fmt.Errorf("module %s is not a listener wrapper", mod)
+					}
+					jsonListenerWrapper := caddyconfig.JSONModuleObject(
+						listenerWrapper,
+						"wrapper",
+						listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
+						nil,
+					)
+					serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
+				}
+
+			case "timeouts":
+				for nesting := d.Nesting(); d.NextBlock(nesting); {
+					switch d.Val() {
+					case "read_body":
+						if !d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						dur, err := caddy.ParseDuration(d.Val())
+						if err != nil {
+							return nil, d.Errf("parsing read_body timeout duration: %v", err)
+						}
+						serverOpts.ReadTimeout = caddy.Duration(dur)
+
+					case "read_header":
+						if !d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						dur, err := caddy.ParseDuration(d.Val())
+						if err != nil {
+							return nil, d.Errf("parsing read_header timeout duration: %v", err)
+						}
+						serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
+
+					case "write":
+						if !d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						dur, err := caddy.ParseDuration(d.Val())
+						if err != nil {
+							return nil, d.Errf("parsing write timeout duration: %v", err)
+						}
+						serverOpts.WriteTimeout = caddy.Duration(dur)
+
+					case "idle":
+						if !d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						dur, err := caddy.ParseDuration(d.Val())
+						if err != nil {
+							return nil, d.Errf("parsing idle timeout duration: %v", err)
+						}
+						serverOpts.IdleTimeout = caddy.Duration(dur)
+
+					default:
+						return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
+					}
+				}
+
+			case "max_header_size":
+				var sizeStr string
+				if !d.AllArgs(&sizeStr) {
+					return nil, d.ArgErr()
+				}
+				size, err := humanize.ParseBytes(sizeStr)
+				if err != nil {
+					return nil, d.Errf("parsing max_header_size: %v", err)
+				}
+				serverOpts.MaxHeaderBytes = int(size)
+
+			case "protocol":
+				for nesting := d.Nesting(); d.NextBlock(nesting); {
+					switch d.Val() {
+					case "allow_h2c":
+						if d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						serverOpts.AllowH2C = true
+
+					case "experimental_http3":
+						if d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						serverOpts.ExperimentalHTTP3 = true
+
+					case "strict_sni_host":
+						if d.NextArg() {
+							return nil, d.ArgErr()
+						}
+						trueBool := true
+						serverOpts.StrictSNIHost = &trueBool
+
+					default:
+						return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
+					}
+				}
+
+			default:
+				return nil, d.Errf("unrecognized servers option '%s'", d.Val())
+			}
+		}
+	}
+	return serverOpts, nil
+}
+
+// applyServerOptions sets the server options on the appropriate servers
+func applyServerOptions(
+	servers map[string]*caddyhttp.Server,
+	options map[string]interface{},
+	warnings *[]caddyconfig.Warning,
+) error {
+	// If experimental HTTP/3 is enabled, enable it on each server.
+	// We already know there won't be a conflict with serverOptions because
+	// we validated earlier that "experimental_http3" cannot be set at the same
+	// time as "servers"
+	if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
+		*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
+		for _, srv := range servers {
+			srv.ExperimentalHTTP3 = true
+		}
+	}
+
+	serverOpts, ok := options["servers"].([]serverOptions)
+	if !ok {
+		return nil
+	}
+
+	for _, server := range servers {
+		// find the options that apply to this server
+		opts := func() *serverOptions {
+			for _, entry := range serverOpts {
+				if entry.ListenerAddress == "" {
+					return &entry
+				}
+				for _, listener := range server.Listen {
+					if entry.ListenerAddress == listener {
+						return &entry
+					}
+				}
+			}
+			return nil
+		}()
+
+		// if none apply, then move to the next server
+		if opts == nil {
+			continue
+		}
+
+		// set all the options
+		server.ListenerWrappersRaw = opts.ListenerWrappersRaw
+		server.ReadTimeout = opts.ReadTimeout
+		server.ReadHeaderTimeout = opts.ReadHeaderTimeout
+		server.WriteTimeout = opts.WriteTimeout
+		server.IdleTimeout = opts.IdleTimeout
+		server.MaxHeaderBytes = opts.MaxHeaderBytes
+		server.AllowH2C = opts.AllowH2C
+		server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
+		server.StrictSNIHost = opts.StrictSNIHost
+	}
+
+	return nil
+}
diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_multi.txt b/caddytest/integration/caddyfile_adapt/global_server_options_multi.txt
new file mode 100644
index 000000000..653eee53c
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/global_server_options_multi.txt
@@ -0,0 +1,83 @@
+{
+	servers {
+		timeouts {
+			idle 90s
+		}
+	}
+	servers :80 {
+		timeouts {
+			idle 60s
+		}
+	}
+	servers :443 {
+		timeouts {
+			idle 30s
+		}
+	}
+}
+
+foo.com {	
+}
+
+http://bar.com {
+}
+
+:8080 {
+}
+
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"idle_timeout": 30000000000,
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"foo.com"
+									]
+								}
+							],
+							"terminal": true
+						}
+					]
+				},
+				"srv1": {
+					"listen": [
+						":80"
+					],
+					"idle_timeout": 60000000000,
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"bar.com"
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"automatic_https": {
+						"skip": [
+							"bar.com"
+						]
+					}
+				},
+				"srv2": {
+					"listen": [
+						":8080"
+					],
+					"idle_timeout": 90000000000
+				}
+			}
+		}
+	}
+}
diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_single.txt b/caddytest/integration/caddyfile_adapt/global_server_options_single.txt
new file mode 100644
index 000000000..5a5c64cd6
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/global_server_options_single.txt
@@ -0,0 +1,62 @@
+{
+	servers {
+		listener_wrappers {
+			tls
+		}
+		timeouts {
+			read_body 30s
+			read_header 30s
+			write 30s
+			idle 30s
+		}
+		max_header_size 100MB
+		protocol {
+			allow_h2c
+			experimental_http3
+			strict_sni_host
+		}
+	}
+}
+
+foo.com {	
+}
+
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"listener_wrappers": [
+						{
+							"wrapper": "tls"
+						}
+					],
+					"read_timeout": 30000000000,
+					"read_header_timeout": 30000000000,
+					"write_timeout": 30000000000,
+					"idle_timeout": 30000000000,
+					"max_header_bytes": 100000000,
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"foo.com"
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"strict_sni_host": true,
+					"experimental_http3": true,
+					"allow_h2c": true
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/handle_path.txt b/caddytest/integration/caddyfile_adapt/handle_path.txt
index 7f40fcf2e..f88174339 100644
--- a/caddytest/integration/caddyfile_adapt/handle_path.txt
+++ b/caddytest/integration/caddyfile_adapt/handle_path.txt
@@ -1,52 +1,52 @@
-:80
-handle_path /api/v1/* {
-	respond "API v1"
-}
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":80"
-					],
-					"routes": [
-						{
-							"match": [
-								{
-									"path": [
-										"/api/v1/*"
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"handler": "rewrite",
-													"strip_path_prefix": "/api/v1"
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"body": "API v1",
-													"handler": "static_response"
-												}
-											]
-										}
-									]
-								}
-							]
-						}
-					]
-				}
-			}
-		}
-	}
+:80
+handle_path /api/v1/* {
+	respond "API v1"
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"path": [
+										"/api/v1/*"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"handler": "rewrite",
+													"strip_path_prefix": "/api/v1"
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"body": "API v1",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt b/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt
index 3258dc9b2..0a89f2ae5 100644
--- a/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt
+++ b/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt
@@ -1,105 +1,105 @@
-:80 {
-	handle /api/* {
-		respond "api"
-	}
-
-	handle_path /static/* {
-		respond "static"
-	}
-
-	handle {
-		respond "handle"
-	}
-}
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":80"
-					],
-					"routes": [
-						{
-							"group": "group3",
-							"match": [
-								{
-									"path": [
-										"/static/*"
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"handler": "rewrite",
-													"strip_path_prefix": "/static"
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"body": "static",
-													"handler": "static_response"
-												}
-											]
-										}
-									]
-								}
-							]
-						},
-						{
-							"group": "group3",
-							"match": [
-								{
-									"path": [
-										"/api/*"
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"body": "api",
-													"handler": "static_response"
-												}
-											]
-										}
-									]
-								}
-							]
-						},
-						{
-							"group": "group3",
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"body": "handle",
-													"handler": "static_response"
-												}
-											]
-										}
-									]
-								}
-							]
-						}
-					]
-				}
-			}
-		}
-	}
+:80 {
+	handle /api/* {
+		respond "api"
+	}
+
+	handle_path /static/* {
+		respond "static"
+	}
+
+	handle {
+		respond "handle"
+	}
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"routes": [
+						{
+							"group": "group3",
+							"match": [
+								{
+									"path": [
+										"/static/*"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"handler": "rewrite",
+													"strip_path_prefix": "/static"
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"body": "static",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							]
+						},
+						{
+							"group": "group3",
+							"match": [
+								{
+									"path": [
+										"/api/*"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "api",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							]
+						},
+						{
+							"group": "group3",
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "handle",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/import_args_file.txt b/caddytest/integration/caddyfile_adapt/import_args_file.txt
index 6947f6812..1eb78f191 100644
--- a/caddytest/integration/caddyfile_adapt/import_args_file.txt
+++ b/caddytest/integration/caddyfile_adapt/import_args_file.txt
@@ -1,49 +1,49 @@
-example.com
-
-import testdata/import_respond.txt Groot Rocket
-import testdata/import_respond.txt you "the confused man"
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":443"
-					],
-					"routes": [
-						{
-							"match": [
-								{
-									"host": [
-										"example.com"
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"body": "'I am Groot', hears Rocket",
-													"handler": "static_response"
-												},
-												{
-													"body": "'I am you', hears the confused man",
-													"handler": "static_response"
-												}
-											]
-										}
-									]
-								}
-							],
-							"terminal": true
-						}
-					]
-				}
-			}
-		}
-	}
+example.com
+
+import testdata/import_respond.txt Groot Rocket
+import testdata/import_respond.txt you "the confused man"
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "'I am Groot', hears Rocket",
+													"handler": "static_response"
+												},
+												{
+													"body": "'I am you', hears the confused man",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/import_args_snippet.txt b/caddytest/integration/caddyfile_adapt/import_args_snippet.txt
index 8d2ff34a4..9fce9abe0 100644
--- a/caddytest/integration/caddyfile_adapt/import_args_snippet.txt
+++ b/caddytest/integration/caddyfile_adapt/import_args_snippet.txt
@@ -1,83 +1,83 @@
-(logging) {
-	log {
-		output file /var/log/caddy/{args.0}.access.log
-	}
-}
-
-a.example.com {
-	import logging a.example.com
-}
-
-b.example.com {
-	import logging b.example.com
-}
-----------
-{
-	"logging": {
-		"logs": {
-			"default": {
-				"exclude": [
-					"http.log.access.log0",
-					"http.log.access.log1"
-				]
-			},
-			"log0": {
-				"writer": {
-					"filename": "/var/log/caddy/a.example.com.access.log",
-					"output": "file"
-				},
-				"include": [
-					"http.log.access.log0"
-				]
-			},
-			"log1": {
-				"writer": {
-					"filename": "/var/log/caddy/b.example.com.access.log",
-					"output": "file"
-				},
-				"include": [
-					"http.log.access.log1"
-				]
-			}
-		}
-	},
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":443"
-					],
-					"routes": [
-						{
-							"match": [
-								{
-									"host": [
-										"a.example.com"
-									]
-								}
-							],
-							"terminal": true
-						},
-						{
-							"match": [
-								{
-									"host": [
-										"b.example.com"
-									]
-								}
-							],
-							"terminal": true
-						}
-					],
-					"logs": {
-						"logger_names": {
-							"a.example.com": "log0",
-							"b.example.com": "log1"
-						}
-					}
-				}
-			}
-		}
-	}
+(logging) {
+	log {
+		output file /var/log/caddy/{args.0}.access.log
+	}
+}
+
+a.example.com {
+	import logging a.example.com
+}
+
+b.example.com {
+	import logging b.example.com
+}
+----------
+{
+	"logging": {
+		"logs": {
+			"default": {
+				"exclude": [
+					"http.log.access.log0",
+					"http.log.access.log1"
+				]
+			},
+			"log0": {
+				"writer": {
+					"filename": "/var/log/caddy/a.example.com.access.log",
+					"output": "file"
+				},
+				"include": [
+					"http.log.access.log0"
+				]
+			},
+			"log1": {
+				"writer": {
+					"filename": "/var/log/caddy/b.example.com.access.log",
+					"output": "file"
+				},
+				"include": [
+					"http.log.access.log1"
+				]
+			}
+		}
+	},
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"a.example.com"
+									]
+								}
+							],
+							"terminal": true
+						},
+						{
+							"match": [
+								{
+									"host": [
+										"b.example.com"
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"logs": {
+						"logger_names": {
+							"a.example.com": "log0",
+							"b.example.com": "log1"
+						}
+					}
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/log_filters.txt b/caddytest/integration/caddyfile_adapt/log_filters.txt
index 549f4e6a0..ab118074c 100644
--- a/caddytest/integration/caddyfile_adapt/log_filters.txt
+++ b/caddytest/integration/caddyfile_adapt/log_filters.txt
@@ -1,69 +1,69 @@
-:80
-
-log {
-	output stdout
-	format filter {
-		wrap console
-		fields {
-			request>headers>Authorization delete
-			request>headers>Server delete
-			request>remote_addr ip_mask {
-				ipv4 24
-				ipv6 32
-			}
-		}
-	}
-}
-----------
-{
-	"logging": {
-		"logs": {
-			"default": {
-				"exclude": [
-					"http.log.access.log0"
-				]
-			},
-			"log0": {
-				"writer": {
-					"output": "stdout"
-				},
-				"encoder": {
-					"fields": {
-						"request\u003eheaders\u003eAuthorization": {
-							"filter": "delete"
-						},
-						"request\u003eheaders\u003eServer": {
-							"filter": "delete"
-						},
-						"request\u003eremote_addr": {
-							"filter": "ip_mask",
-							"ipv4_cidr": 24,
-							"ipv6_cidr": 32
-						}
-					},
-					"format": "filter",
-					"wrap": {
-						"format": "console"
-					}
-				},
-				"include": [
-					"http.log.access.log0"
-				]
-			}
-		}
-	},
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":80"
-					],
-					"logs": {
-						"default_logger_name": "log0"
-					}
-				}
-			}
-		}
-	}
+:80
+
+log {
+	output stdout
+	format filter {
+		wrap console
+		fields {
+			request>headers>Authorization delete
+			request>headers>Server delete
+			request>remote_addr ip_mask {
+				ipv4 24
+				ipv6 32
+			}
+		}
+	}
+}
+----------
+{
+	"logging": {
+		"logs": {
+			"default": {
+				"exclude": [
+					"http.log.access.log0"
+				]
+			},
+			"log0": {
+				"writer": {
+					"output": "stdout"
+				},
+				"encoder": {
+					"fields": {
+						"request\u003eheaders\u003eAuthorization": {
+							"filter": "delete"
+						},
+						"request\u003eheaders\u003eServer": {
+							"filter": "delete"
+						},
+						"request\u003eremote_addr": {
+							"filter": "ip_mask",
+							"ipv4_cidr": 24,
+							"ipv6_cidr": 32
+						}
+					},
+					"format": "filter",
+					"wrap": {
+						"format": "console"
+					}
+				},
+				"include": [
+					"http.log.access.log0"
+				]
+			}
+		}
+	},
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"logs": {
+						"default_logger_name": "log0"
+					}
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt
index d45312894..bb7c7f7b9 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt
@@ -1,132 +1,132 @@
-:8886
-
-route {
-	# Add trailing slash for directory requests
-	@canonicalPath {
-		file {
-			try_files {path}/index.php
-		}
-		not path */
-	}
-	redir @canonicalPath {path}/ 308
-
-	# If the requested file does not exist, try index files
-	@indexFiles {
-		file {
-			try_files {path} {path}/index.php index.php
-			split_path .php
-		}
-	}
-	rewrite @indexFiles {http.matchers.file.relative}
-
-	# Proxy PHP files to the FastCGI responder
-	@phpFiles {
-		path *.php
-	}
-	reverse_proxy @phpFiles 127.0.0.1:9000 {
-		transport fastcgi {
-			split .php
-		}
-	}
-}
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":8886"
-					],
-					"routes": [
-						{
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"handler": "static_response",
-													"headers": {
-														"Location": [
-															"{http.request.uri.path}/"
-														]
-													},
-													"status_code": 308
-												}
-											],
-											"match": [
-												{
-													"file": {
-														"try_files": [
-															"{http.request.uri.path}/index.php"
-														]
-													},
-													"not": [
-														{
-															"path": [
-																"*/"
-															]
-														}
-													]
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"handler": "rewrite",
-													"uri": "{http.matchers.file.relative}"
-												}
-											],
-											"match": [
-												{
-													"file": {
-														"split_path": [
-															".php"
-														],
-														"try_files": [
-															"{http.request.uri.path}",
-															"{http.request.uri.path}/index.php",
-															"index.php"
-														]
-													}
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"handler": "reverse_proxy",
-													"transport": {
-														"protocol": "fastcgi",
-														"split_path": [
-															".php"
-														]
-													},
-													"upstreams": [
-														{
-															"dial": "127.0.0.1:9000"
-														}
-													]
-												}
-											],
-											"match": [
-												{
-													"path": [
-														"*.php"
-													]
-												}
-											]
-										}
-									]
-								}
-							]
-						}
-					]
-				}
-			}
-		}
-	}
+:8886
+
+route {
+	# Add trailing slash for directory requests
+	@canonicalPath {
+		file {
+			try_files {path}/index.php
+		}
+		not path */
+	}
+	redir @canonicalPath {path}/ 308
+
+	# If the requested file does not exist, try index files
+	@indexFiles {
+		file {
+			try_files {path} {path}/index.php index.php
+			split_path .php
+		}
+	}
+	rewrite @indexFiles {http.matchers.file.relative}
+
+	# Proxy PHP files to the FastCGI responder
+	@phpFiles {
+		path *.php
+	}
+	reverse_proxy @phpFiles 127.0.0.1:9000 {
+		transport fastcgi {
+			split .php
+		}
+	}
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":8886"
+					],
+					"routes": [
+						{
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"handler": "static_response",
+													"headers": {
+														"Location": [
+															"{http.request.uri.path}/"
+														]
+													},
+													"status_code": 308
+												}
+											],
+											"match": [
+												{
+													"file": {
+														"try_files": [
+															"{http.request.uri.path}/index.php"
+														]
+													},
+													"not": [
+														{
+															"path": [
+																"*/"
+															]
+														}
+													]
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"handler": "rewrite",
+													"uri": "{http.matchers.file.relative}"
+												}
+											],
+											"match": [
+												{
+													"file": {
+														"split_path": [
+															".php"
+														],
+														"try_files": [
+															"{http.request.uri.path}",
+															"{http.request.uri.path}/index.php",
+															"index.php"
+														]
+													}
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"handler": "reverse_proxy",
+													"transport": {
+														"protocol": "fastcgi",
+														"split_path": [
+															".php"
+														]
+													},
+													"upstreams": [
+														{
+															"dial": "127.0.0.1:9000"
+														}
+													]
+												}
+											],
+											"match": [
+												{
+													"path": [
+														"*.php"
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt
index 2f4e6fe5c..488c52528 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt
@@ -1,112 +1,112 @@
-:8884
-
-@api host example.com
-php_fastcgi @api localhost:9000
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":8884"
-					],
-					"routes": [
-						{
-							"match": [
-								{
-									"host": [
-										"example.com"
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "subroute",
-									"routes": [
-										{
-											"handle": [
-												{
-													"handler": "static_response",
-													"headers": {
-														"Location": [
-															"{http.request.uri.path}/"
-														]
-													},
-													"status_code": 308
-												}
-											],
-											"match": [
-												{
-													"file": {
-														"try_files": [
-															"{http.request.uri.path}/index.php"
-														]
-													},
-													"not": [
-														{
-															"path": [
-																"*/"
-															]
-														}
-													]
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"handler": "rewrite",
-													"uri": "{http.matchers.file.relative}"
-												}
-											],
-											"match": [
-												{
-													"file": {
-														"split_path": [
-															".php"
-														],
-														"try_files": [
-															"{http.request.uri.path}",
-															"{http.request.uri.path}/index.php",
-															"index.php"
-														]
-													}
-												}
-											]
-										},
-										{
-											"handle": [
-												{
-													"handler": "reverse_proxy",
-													"transport": {
-														"protocol": "fastcgi",
-														"split_path": [
-															".php"
-														]
-													},
-													"upstreams": [
-														{
-															"dial": "localhost:9000"
-														}
-													]
-												}
-											],
-											"match": [
-												{
-													"path": [
-														"*.php"
-													]
-												}
-											]
-										}
-									]
-								}
-							]
-						}
-					]
-				}
-			}
-		}
-	}
+:8884
+
+@api host example.com
+php_fastcgi @api localhost:9000
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":8884"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"handler": "static_response",
+													"headers": {
+														"Location": [
+															"{http.request.uri.path}/"
+														]
+													},
+													"status_code": 308
+												}
+											],
+											"match": [
+												{
+													"file": {
+														"try_files": [
+															"{http.request.uri.path}/index.php"
+														]
+													},
+													"not": [
+														{
+															"path": [
+																"*/"
+															]
+														}
+													]
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"handler": "rewrite",
+													"uri": "{http.matchers.file.relative}"
+												}
+											],
+											"match": [
+												{
+													"file": {
+														"split_path": [
+															".php"
+														],
+														"try_files": [
+															"{http.request.uri.path}",
+															"{http.request.uri.path}/index.php",
+															"index.php"
+														]
+													}
+												}
+											]
+										},
+										{
+											"handle": [
+												{
+													"handler": "reverse_proxy",
+													"transport": {
+														"protocol": "fastcgi",
+														"split_path": [
+															".php"
+														]
+													},
+													"upstreams": [
+														{
+															"dial": "localhost:9000"
+														}
+													]
+												}
+											],
+											"match": [
+												{
+													"path": [
+														"*.php"
+													]
+												}
+											]
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt b/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt
index 6203a89cc..3859a7e54 100644
--- a/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt
+++ b/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt
@@ -1,51 +1,51 @@
-:80
-
-respond 200
-
-@untrusted not remote_ip 10.1.1.0/24
-respond @untrusted 401
-----------
-{
-	"apps": {
-		"http": {
-			"servers": {
-				"srv0": {
-					"listen": [
-						":80"
-					],
-					"routes": [
-						{
-							"match": [
-								{
-									"not": [
-										{
-											"remote_ip": {
-												"ranges": [
-													"10.1.1.0/24"
-												]
-											}
-										}
-									]
-								}
-							],
-							"handle": [
-								{
-									"handler": "static_response",
-									"status_code": 401
-								}
-							]
-						},
-						{
-							"handle": [
-								{
-									"handler": "static_response",
-									"status_code": 200
-								}
-							]
-						}
-					]
-				}
-			}
-		}
-	}
+:80
+
+respond 200
+
+@untrusted not remote_ip 10.1.1.0/24
+respond @untrusted 401
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"not": [
+										{
+											"remote_ip": {
+												"ranges": [
+													"10.1.1.0/24"
+												]
+											}
+										}
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "static_response",
+									"status_code": 401
+								}
+							]
+						},
+						{
+							"handle": [
+								{
+									"handler": "static_response",
+									"status_code": 200
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 485afe097..d93a5c8a2 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -23,6 +23,7 @@ import (
 	"strconv"
 
 	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 )
 
 func init() {
@@ -231,6 +232,8 @@ func (tlsPlaceholderWrapper) CaddyModule() caddy.ModuleInfo {
 
 func (tlsPlaceholderWrapper) WrapListener(ln net.Listener) net.Listener { return ln }
 
+func (tlsPlaceholderWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil }
+
 const (
 	// DefaultHTTPPort is the default port for HTTP.
 	DefaultHTTPPort = 80
@@ -241,3 +244,4 @@ const (
 
 // Interface guard
 var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
+var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)