diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 2bcdc85f4..19f6bf29f 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -99,13 +99,15 @@ type (
 		cidrs []*net.IPNet
 	}
 
-	// MatchNot matches requests by negating its matchers' results.
-	// To use, simply specify a set of matchers like you normally would;
-	// the only difference is that their result will be negated.
+	// MatchNot matches requests by negating the results of its matcher
+	// sets. A single "not" matcher takes one or more matcher sets. Each
+	// matcher set is OR'ed; in other words, if any matcher set returns
+	// true, the final result of the "not" matcher is false. Individual
+	// matchers within a set work the same (i.e. different matchers in
+	// the same set are AND'ed).
 	MatchNot struct {
-		MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"`
-
-		Matchers MatcherSet `json:"-"`
+		MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"`
+		MatcherSets    []MatcherSet      `json:"-"`
 	}
 )
 
@@ -538,25 +540,16 @@ func (MatchNot) CaddyModule() caddy.ModuleInfo {
 	}
 }
 
-// UnmarshalJSON unmarshals data into m's unexported map field.
-// This is done because we cannot embed the map directly into
-// the struct, but we need a struct because we need another
-// field just for the provisioned modules.
-func (m *MatchNot) UnmarshalJSON(data []byte) error {
-	return json.Unmarshal(data, &m.MatchersRaw)
-}
-
-// MarshalJSON marshals m's matchers.
-func (m MatchNot) MarshalJSON() ([]byte, error) {
-	return json.Marshal(m.MatchersRaw)
-}
-
 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	// first, unmarshal each matcher in the set from its tokens
-
-	matcherMap := make(map[string]RequestMatcher)
+	type matcherPair struct {
+		raw     caddy.ModuleMap
+		decoded MatcherSet
+	}
 	for d.Next() {
+		var mp matcherPair
+		matcherMap := make(map[string]RequestMatcher)
 		for d.NextBlock(0) {
 			matcherName := d.Val()
 			mod, err := caddy.GetModule("http.matchers." + matcherName)
@@ -572,42 +565,64 @@ func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 				return err
 			}
 			rm := unm.(RequestMatcher)
-			m.Matchers = append(m.Matchers, rm)
 			matcherMap[matcherName] = rm
+			mp.decoded = append(mp.decoded, rm)
 		}
-	}
 
-	// we should now be functional, but we also need
-	// to be able to marshal as JSON, otherwise config
-	// adaptation won't work properly
-	m.MatchersRaw = make(caddy.ModuleMap)
-	for name, matchers := range matcherMap {
-		jsonBytes, err := json.Marshal(matchers)
-		if err != nil {
-			return fmt.Errorf("marshaling matcher %s: %v", name, err)
+		// we should now have a functional 'not' matcher, but we also
+		// need to be able to marshal as JSON, otherwise config
+		// adaptation will be missing the matchers!
+		mp.raw = make(caddy.ModuleMap)
+		for name, matcher := range matcherMap {
+			jsonBytes, err := json.Marshal(matcher)
+			if err != nil {
+				return fmt.Errorf("marshaling %T matcher: %v", matcher, err)
+			}
+			mp.raw[name] = jsonBytes
 		}
-		m.MatchersRaw[name] = jsonBytes
+		m.MatcherSetsRaw = append(m.MatcherSetsRaw, mp.raw)
 	}
-
 	return nil
 }
 
+// UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON
+// bytes directly into m's MatcherSetsRaw field.
+func (m *MatchNot) UnmarshalJSON(data []byte) error {
+	return json.Unmarshal(data, &m.MatcherSetsRaw)
+}
+
+// MarshalJSON satisfies json.Marshaler by marshaling
+// m's raw matcher sets.
+func (m MatchNot) MarshalJSON() ([]byte, error) {
+	return json.Marshal(m.MatcherSetsRaw)
+}
+
 // Provision loads the matcher modules to be negated.
 func (m *MatchNot) Provision(ctx caddy.Context) error {
-	mods, err := ctx.LoadModule(m, "MatchersRaw")
+	matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw")
 	if err != nil {
-		return fmt.Errorf("loading matchers: %v", err)
+		return fmt.Errorf("loading matcher sets: %v", err)
 	}
-	for _, modIface := range mods.(map[string]interface{}) {
-		m.Matchers = append(m.Matchers, modIface.(RequestMatcher))
+	for _, modMap := range matcherSets.([]map[string]interface{}) {
+		var ms MatcherSet
+		for _, modIface := range modMap {
+			ms = append(ms, modIface.(RequestMatcher))
+		}
+		m.MatcherSets = append(m.MatcherSets, ms)
 	}
 	return nil
 }
 
-// Match returns true if r matches m. Since this matcher negates the
-// embedded matchers, false is returned if any of its matchers match.
+// Match returns true if r matches m. Since this matcher negates
+// the embedded matchers, false is returned if any of its matcher
+// sets return true.
 func (m MatchNot) Match(r *http.Request) bool {
-	return !m.Matchers.Match(r)
+	for _, ms := range m.MatcherSets {
+		if ms.Match(r) {
+			return false
+		}
+	}
+	return true
 }
 
 // CaddyModule returns the Caddy module information.
diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go
index 1e50776e5..021bb9838 100644
--- a/modules/caddyhttp/matchers_test.go
+++ b/modules/caddyhttp/matchers_test.go
@@ -823,6 +823,119 @@ func TestResponseMatcher(t *testing.T) {
 	}
 }
 
+func TestNotMatcher(t *testing.T) {
+	for i, tc := range []struct {
+		host, path string
+		match      MatchNot
+		expect     bool
+	}{
+		{
+			host: "example.com", path: "/",
+			match:  MatchNot{},
+			expect: true,
+		},
+		{
+			host: "example.com", path: "/foo",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/foo"},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			host: "example.com", path: "/bar",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/foo"},
+					},
+				},
+			},
+			expect: true,
+		},
+		{
+			host: "example.com", path: "/bar",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/foo"},
+					},
+					{
+						MatchHost{"example.com"},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			host: "example.com", path: "/bar",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/bar"},
+					},
+					{
+						MatchHost{"example.com"},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			host: "example.com", path: "/foo",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/bar"},
+					},
+					{
+						MatchHost{"sub.example.com"},
+					},
+				},
+			},
+			expect: true,
+		},
+		{
+			host: "example.com", path: "/foo",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/foo"},
+						MatchHost{"example.com"},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			host: "example.com", path: "/foo",
+			match: MatchNot{
+				MatcherSets: []MatcherSet{
+					{
+						MatchPath{"/bar"},
+						MatchHost{"example.com"},
+					},
+				},
+			},
+			expect: true,
+		},
+	} {
+		req := &http.Request{Host: tc.host, URL: &url.URL{Path: tc.path}}
+		repl := caddy.NewReplacer()
+		ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+		req = req.WithContext(ctx)
+
+		actual := tc.match.Match(req)
+		if actual != tc.expect {
+			t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path)
+			continue
+		}
+	}
+}
+
 func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
 	req := &http.Request{Host: "localhost"}
 	repl := caddy.NewReplacer()
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index 2eb1f3dc4..fdf981886 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -128,8 +128,10 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
 			TryFiles: []string{"{http.request.uri.path}/index.php"},
 		}),
 		"not": h.JSON(caddyhttp.MatchNot{
-			MatchersRaw: caddy.ModuleMap{
-				"path": h.JSON(caddyhttp.MatchPath{"*/"}),
+			MatcherSetsRaw: []caddy.ModuleMap{
+				{
+					"path": h.JSON(caddyhttp.MatchPath{"*/"}),
+				},
 			},
 		}),
 	}