Repository: avenga/couper
Pull request #437 (2022-02-10 13:00:07)

Multi-file-config
reads and merges configuration files from a given directory 

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>


johakoch: Needs test using config files in server/testdata/multi/server.
johakoch: Does the merging take **all** block labels into account?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0265b56e..1bf47dca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
 
 Unreleased changes are available as `avenga/couper:edge` container.
 
+* **Added**
+  * Couper [reads and merges configuration files](./docs/CLI.md#global-options) from a given directory ([#437](https://github.com/avenga/couper/pull/437))
+    * provided via `-d` command-line flag or `COUPER_FILE_DIRECTORY` environment variable
+
 ---
 
 ## [1.8.0](https://github.com/avenga/couper/releases/tag/v1.8.0)
diff --git a/DOCKER.md b/DOCKER.md
index 578c5698..5cc7ba8a 100644
--- a/DOCKER.md
+++ b/DOCKER.md
@@ -33,6 +33,7 @@ docker run avenga/couper run -watch -p 8081
 | Variable                             | Default | Description |
 |:-------------------------------------| :------ | :---------- |
 | COUPER_FILE                          | `couper.hcl` | Path to the configuration file. |
+| COUPER_FILE_DIRECTORY                | `""`    | Path to the configuration files directory. |
 | COUPER_ACCEPT_FORWARDED_URL          | `""`    | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](https://github.com/avenga/couper/blob/master/docs/REFERENCE.md#request) `url`, `origin`, `protocol`, `host`, `port`. Comma-separated list of values. Valid values: `proto`, `host`, `port`. |
 | COUPER_DEFAULT_PORT                  | `8080`  | Sets the default port to the given value and does not override explicit `[host:port]` configurations from file. |
 | COUPER_HEALTH_PATH                   | `/healthz` | Path for health-check requests for all servers and ports. |
diff --git a/command/verify.go b/command/verify.go
index 8d1463f7..024db276 100644
--- a/command/verify.go
+++ b/command/verify.go
@@ -20,7 +20,7 @@ func NewVerify() *Verify {
 }
 
 func (v Verify) Execute(args Args, _ *config.Couper, logger *logrus.Entry) error {
-	if len(args) != 1 {
+	if len(args) != 2 {
 		v.Usage()
 
 		err := fmt.Errorf("invalid number of arguments given")
@@ -29,7 +29,7 @@ func (v Verify) Execute(args Args, _ *config.Couper, logger *logrus.Entry) error
 		return err
 	}
 
-	cf, err := configload.LoadFile(args[0])
+	cf, err := configload.LoadFiles(args[0], args[1])
 	if diags, ok := err.(hcl.Diagnostics); ok {
 		for _, diag := range diags {
 			logger.WithError(diag).Error()
@@ -54,5 +54,5 @@ func (v Verify) Execute(args Args, _ *config.Couper, logger *logrus.Entry) error
 }
 
 func (v Verify) Usage() {
-	println("Usage of verify:\n  verify [-f <file>]	Verify the syntax of the given configuration file.")
+	println("Usage of verify:\n  verify [-f <file>]	[-d <dir>]	Verify the syntax of the given configuration file(s).")
 }
diff --git a/config/configload/load.go b/config/configload/load.go
index 1df353b1..66054fff 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -2,12 +2,16 @@ package configload
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"regexp"
+	"strings"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
+	"github.com/hashicorp/hcl/v2/hclparse"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload/collect"
@@ -35,12 +39,44 @@ const (
 )
 
 var regexProxyRequestLabel = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
+var defaultsConfig *config.Defaults
+var evalContext *eval.Context
 var envContext *hcl.EvalContext
 
 func init() {
 	envContext = eval.NewContext(nil, nil).HCLContext()
 }
 
+func updateContext(body hcl.Body, srcBytes [][]byte) hcl.Diagnostics {
+	defaultsBlock := &config.DefaultsBlock{}
+	if diags := gohcl.DecodeBody(body, nil, defaultsBlock); diags.HasErrors() {
+		return diags
+	}
+
+	// We need the "envContext" to be able to resolve abs pathes in the config.
+	defaultsConfig = defaultsBlock.Defaults
+	evalContext = eval.NewContext(srcBytes, defaultsConfig)
+	envContext = evalContext.HCLContext()
+
+	return nil
+}
+
+func parseFile(filePath string, srcBytes *[][]byte) (*hcl.File, error) {
+	src, err := os.ReadFile(filePath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load configuration: %w", err)
+	}
+
+	*srcBytes = append(*srcBytes, src)
+
+	parsed, diags := hclparse.NewParser().ParseHCLFile(filePath)
+	if diags.HasErrors() {
+		return nil, diags
+	}
+
+	return parsed, nil
+}
+
 // SetWorkingDirectory sets the working directory to the given configuration file path.
 func SetWorkingDirectory(configFile string) (string, error) {
 	if err := os.Chdir(filepath.Dir(configFile)); err != nil {
@@ -50,20 +86,114 @@ func SetWorkingDirectory(configFile string) (string, error) {
 	return os.Getwd()
 }
 
-func LoadFile(filePath string) (*config.Couper, error) {
-	_, err := SetWorkingDirectory(filePath)
+func LoadFiles(filePath, dirPath string) (*config.Couper, error) {
+	var (
+		srcBytes     [][]byte
+		parsedBodies []*hclsyntax.Body
+		hasIndexHCL  bool
+	)
+
+	if dirPath != "" {
+		dir, err := filepath.Abs(dirPath)
+		if err != nil {
+			return nil, err
+		}
+
+		dirPath = dir
+
+		// ReadDir ... returns a list ... sorted by filename.
+		listing, err := ioutil.ReadDir(dirPath)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, file := range listing {
+			if file.IsDir() || !strings.HasSuffix(file.Name(), ".hcl") {
+				continue
+			}
+
+			if file.Name() == config.DefaultFilename {
+				hasIndexHCL = true
+				continue
+			}
+
+			parsed, err := parseFile(dirPath+"/"+file.Name(), &srcBytes)
+			if err != nil {
+				return nil, err
+			}
+
+			parsedBodies = append(parsedBodies, parsed.Body.(*hclsyntax.Body))
+		}
+	}
+
+	if hasIndexHCL {
+		parsed, err := parseFile(dirPath+"/"+config.DefaultFilename, &srcBytes)
+		if err != nil {
+			return nil, err
+		}
+
+		parsedBodies = append([]*hclsyntax.Body{parsed.Body.(*hclsyntax.Body)}, parsedBodies...)
+	}
+	if filePath != "" {
+		filePath, err := filepath.Abs(filePath)
+		if err != nil {
+			return nil, err
+		}
+
+		parsed, err := parseFile(filePath, &srcBytes)
+		if err != nil {
+			return nil, err
+		}
+
+		parsedBodies = append([]*hclsyntax.Body{parsed.Body.(*hclsyntax.Body)}, parsedBodies...)
+
+		_, err = SetWorkingDirectory(filePath)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		_, err := SetWorkingDirectory(dirPath + "/dummy.hcl")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if len(srcBytes) == 0 {
+		return nil, fmt.Errorf("missing configuration files")
+	}
+
+	defaults := mergeAttributes("defaults", parsedBodies)
+
+	defs := &hclsyntax.Body{
+		Blocks: hclsyntax.Blocks{defaults},
+	}
+
+	if diags := updateContext(defs, srcBytes); diags.HasErrors() {
+		return nil, diags
+	}
+
+	settings := mergeAttributes("settings", parsedBodies)
+
+	definitions, err := mergeDefinitions(parsedBodies)
 	if err != nil {
 		return nil, err
 	}
 
-	filename := filepath.Base(filePath)
-
-	src, err := os.ReadFile(filename)
+	servers, err := mergeServers(parsedBodies)
 	if err != nil {
-		return nil, fmt.Errorf("failed to load configuration: %w", err)
+		return nil, err
 	}
 
-	return LoadBytes(src, filename)
+	configBlocks := servers
+	configBlocks = append(configBlocks, definitions)
+	configBlocks = append(configBlocks, defaults)
+	configBlocks = append(configBlocks, settings)
+
+	configBody := &hclsyntax.Body{
+		Blocks: configBlocks,
+	}
+
+	return LoadConfig(configBody, filepath.Base(filePath), dirPath)
 }
 
 func LoadBytes(src []byte, filename string) (*config.Couper, error) {
@@ -72,31 +202,27 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) {
 		return nil, diags
 	}
 
-	return LoadConfig(hclBody, src, filename)
-}
-
-func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, error) {
-	if diags := ValidateConfigSchema(body, &config.Couper{}, src); diags.HasErrors() {
+	if diags := updateContext(hclBody, [][]byte{src}); diags.HasErrors() {
 		return nil, diags
 	}
 
-	defaultsBlock := &config.DefaultsBlock{}
-	if diags := gohcl.DecodeBody(body, nil, defaultsBlock); diags.HasErrors() {
+	return LoadConfig(hclBody, filename, "")
+}
+
+func LoadConfig(body hcl.Body, filename, dirPath string) (*config.Couper, error) {
+	if diags := ValidateConfigSchema(body, &config.Couper{}); diags.HasErrors() {
 		return nil, diags
 	}
 
 	defaults := config.DefaultSettings
 	defaults.AcceptForwarded = &config.AcceptForwarded{}
 
-	evalContext := eval.NewContext(src, defaultsBlock.Defaults)
-	envContext = evalContext.HCLContext()
-
 	couperConfig := &config.Couper{
-		Bytes:       src,
 		Context:     evalContext,
 		Definitions: &config.Definitions{},
-		Defaults:    defaultsBlock.Defaults,
+		Defaults:    defaultsConfig,
 		Filename:    filename,
+		Dirpath:     dirPath,
 		Settings:    &defaults,
 	}
 
diff --git a/config/configload/merge.go b/config/configload/merge.go
new file mode 100644
index 00000000..924f04f3
--- /dev/null
+++ b/config/configload/merge.go
@@ -0,0 +1,373 @@
+package configload
+
+import (
+	"fmt"
+	"path"
+	"path/filepath"
+
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
+)
+
+func errorUniqueLabels(block *hclsyntax.Block) error {
+	defRange := block.DefRange()
+
+	return hcl.Diagnostics{
+		&hcl.Diagnostic{
+			Severity: hcl.DiagError,
+			Summary:  fmt.Sprintf("All %s blocks must have unique labels.", block.Type),
+			Subject:  &defRange,
+		},
+	}
+}
+
+func absPath(attr *hclsyntax.Attribute) hclsyntax.Expression {
+	value, diags := attr.Expr.Value(envContext)
+	if diags.HasErrors() {
+		return attr.Expr // Return unchanged in error cases.
+	}
+
+	return &hclsyntax.LiteralValueExpr{
+		Val: cty.StringVal(
+			path.Join(filepath.Dir(attr.SrcRange.Filename), value.AsString()),
+		),
+	}
+}
+
+func absBackendBlock(backendBlock *hclsyntax.Block) {
+	for _, block := range backendBlock.Body.Blocks {
+		if block.Type == "openapi" {
+			if attr, ok := block.Body.Attributes["file"]; ok {
+				block.Body.Attributes["file"].Expr = absPath(attr)
+			}
+		} else if block.Type == oauth2 {
+			for _, innerBlock := range block.Body.Blocks {
+				if innerBlock.Type == backend {
+					absBackendBlock(innerBlock) // Recursive call
+				}
+			}
+		}
+	}
+}
+
+func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
+	type (
+		namedBlocks   map[string]*hclsyntax.Block
+		apiDefinition struct {
+			labels       []string
+			attributes   hclsyntax.Attributes
+			blocks       namedBlocks
+			endpoints    namedBlocks
+			errorHandler namedBlocks
+		}
+		namedAPIs        map[string]*apiDefinition
+		serverDefinition struct {
+			labels     []string
+			attributes hclsyntax.Attributes
+			blocks     namedBlocks
+			endpoints  namedBlocks
+			apis       namedAPIs
+		}
+		servers map[string]*serverDefinition
+	)
+
+	/*
+		serverDefinition[<key>] = {
+			attributes       = hclsyntax.Attributes
+			blocks[<name>]   = hclsyntax.Block (cors, files, spa)
+			endpoints[<key>] = hclsyntax.Block
+			apis[<key>]      = {
+				attributes           = hclsyntax.Attributes
+				blocks[<name>]       = hclsyntax.Block (cors)
+				endpoints[<key>]     = hclsyntax.Block
+				error_handler[<key>] = hclsyntax.Block
+			}
+		}
+	*/
+
+	results := make(servers)
+
+	for _, body := range bodies {
+		uniqueServerLabels := make(map[string]struct{})
+
+		for _, outerBlock := range body.Blocks {
+			if outerBlock.Type != server {
+				continue
+			}
+
+			var serverKey string
+
+			if len(outerBlock.Labels) > 0 {
+				serverKey = outerBlock.Labels[0]
+			}
+
+			if len(bodies) > 1 {
+				if _, ok := uniqueServerLabels[serverKey]; ok {
+					return nil, errorUniqueLabels(outerBlock)
+				}
+
+				uniqueServerLabels[serverKey] = struct{}{}
+			} else {
+				// Create unique key for multiple server blocks inside a single config file.
+				serverKey += fmt.Sprintf("|%p", &serverKey)
+			}
+
+			if results[serverKey] == nil {
+				results[serverKey] = &serverDefinition{
+					labels:     outerBlock.Labels,
+					attributes: make(hclsyntax.Attributes),
+					blocks:     make(namedBlocks),
+					endpoints:  make(namedBlocks),
+					apis:       make(namedAPIs),
+				}
+			}
+
+			for name, attr := range outerBlock.Body.Attributes {
+				results[serverKey].attributes[name] = attr
+			}
+
+			if attr, ok := results[serverKey].attributes["error_file"]; ok {
+				results[serverKey].attributes["error_file"].Expr = absPath(attr)
+			}
+
+			for _, block := range outerBlock.Body.Blocks {
+				uniqueAPILabels := make(map[string]struct{})
+
+				// TODO: Do we need this IF around the FOR?
+				if block.Type == "files" || block.Type == "spa" || block.Type == api || block.Type == "endpoint" {
+					for _, name := range []string{"error_file", "document_root"} {
+						if attr, ok := block.Body.Attributes[name]; ok {
+							block.Body.Attributes[name].Expr = absPath(attr)
+						}
+					}
+				}
+
+				if block.Type == api || block.Type == "endpoint" {
+					for _, innerBlock := range block.Body.Blocks {
+						if innerBlock.Type == "error_handler" {
+							if attr, ok := innerBlock.Body.Attributes["error_file"]; ok {
+								innerBlock.Body.Attributes["error_file"].Expr = absPath(attr)
+							}
+						} else if innerBlock.Type == "endpoint" {
+							for _, innerInnerBlock := range innerBlock.Body.Blocks {
+								if innerInnerBlock.Type == backend {
+									absBackendBlock(innerInnerBlock) // Backend block inside a endpoint block in an api block
+								}
+							}
+						}
+					}
+				}
+
+				if block.Type == "endpoint" {
+					if len(block.Labels) == 0 {
+						return nil, errorUniqueLabels(block)
+					}
+
+					results[serverKey].endpoints[block.Labels[0]] = block
+
+					for _, innerBlock := range block.Body.Blocks {
+						if innerBlock.Type == backend {
+							absBackendBlock(innerBlock) // Backend block inside a free endpoint block
+						}
+					}
+				} else if block.Type == api {
+					var apiKey string
+
+					if len(block.Labels) > 0 {
+						apiKey = block.Labels[0]
+					}
+
+					if len(bodies) > 1 {
+						if _, ok := uniqueAPILabels[apiKey]; ok {
+							return nil, errorUniqueLabels(block)
+						}
+
+						uniqueAPILabels[apiKey] = struct{}{}
+					} else {
+						// Create unique key for multiple api blocks inside a single config file.
+						apiKey += fmt.Sprintf("|%p", &apiKey)
+					}
+
+					if results[serverKey].apis[apiKey] == nil {
+						results[serverKey].apis[apiKey] = &apiDefinition{
+							labels:       block.Labels,
+							attributes:   make(hclsyntax.Attributes),
+							blocks:       make(namedBlocks),
+							endpoints:    make(namedBlocks),
+							errorHandler: make(namedBlocks),
+						}
+					}
+
+					for name, attr := range block.Body.Attributes {
+						results[serverKey].apis[apiKey].attributes[name] = attr
+					}
+
+					for _, subBlock := range block.Body.Blocks {
+						if subBlock.Type == "endpoint" {
+							if len(subBlock.Labels) == 0 {
+								return nil, errorUniqueLabels(subBlock)
+							}
+
+							results[serverKey].apis[apiKey].endpoints[subBlock.Labels[0]] = subBlock
+						} else if subBlock.Type == "error_handler" {
+							var ehKey string
+
+							if len(subBlock.Labels) > 0 {
+								ehKey = subBlock.Labels[0]
+							}
+
+							results[serverKey].apis[apiKey].errorHandler[ehKey] = subBlock
+						} else {
+							results[serverKey].apis[apiKey].blocks[subBlock.Type] = subBlock
+						}
+					}
+				} else {
+					results[serverKey].blocks[block.Type] = block
+				}
+			}
+		}
+	}
+
+	var mergedServers hclsyntax.Blocks
+
+	for _, serverBlock := range results {
+		var serverBlocks hclsyntax.Blocks
+
+		for _, b := range serverBlock.blocks {
+			serverBlocks = append(serverBlocks, b)
+		}
+
+		for _, b := range serverBlock.endpoints {
+			serverBlocks = append(serverBlocks, b)
+		}
+
+		for _, apiBlock := range serverBlock.apis {
+			var apiBlocks hclsyntax.Blocks
+
+			for _, b := range apiBlock.blocks {
+				apiBlocks = append(apiBlocks, b)
+			}
+
+			for _, b := range apiBlock.endpoints {
+				apiBlocks = append(apiBlocks, b)
+			}
+
+			for _, b := range apiBlock.errorHandler {
+				apiBlocks = append(apiBlocks, b)
+			}
+
+			mergedAPI := &hclsyntax.Block{
+				Type:   api,
+				Labels: apiBlock.labels,
+				Body: &hclsyntax.Body{
+					Attributes: apiBlock.attributes,
+					Blocks:     apiBlocks,
+				},
+			}
+
+			serverBlocks = append(serverBlocks, mergedAPI)
+		}
+
+		mergedServer := &hclsyntax.Block{
+			Type:   server,
+			Labels: serverBlock.labels,
+			Body: &hclsyntax.Body{
+				Attributes: serverBlock.attributes,
+				Blocks:     serverBlocks,
+			},
+		}
+
+		mergedServers = append(mergedServers, mergedServer)
+	}
+
+	return mergedServers, nil
+}
+
+func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
+	type data map[string]*hclsyntax.Block
+	type list map[string]data
+
+	definitionsBlock := make(list)
+
+	for _, body := range bodies {
+		for _, outerBlock := range body.Blocks {
+			if outerBlock.Type == definitions {
+				for _, innerBlock := range outerBlock.Body.Blocks {
+					if definitionsBlock[innerBlock.Type] == nil {
+						definitionsBlock[innerBlock.Type] = make(data)
+					}
+
+					if len(innerBlock.Labels) == 0 {
+						return nil, errorUniqueLabels(innerBlock)
+					}
+
+					definitionsBlock[innerBlock.Type][innerBlock.Labels[0]] = innerBlock
+
+					// TODO: Do we need this IF around the FOR?
+					if innerBlock.Type == "basic_auth" || innerBlock.Type == "jwt" || innerBlock.Type == "jwt_signing_profile" || innerBlock.Type == "saml" {
+						for _, name := range []string{"htpasswd_file", "key_file", "signing_key_file", "idp_metadata_file"} {
+							if attr, ok := innerBlock.Body.Attributes[name]; ok {
+								innerBlock.Body.Attributes[name].Expr = absPath(attr)
+							}
+						}
+					}
+
+					for _, block := range innerBlock.Body.Blocks {
+						if block.Type == errorHandler {
+							if attr, ok := innerBlock.Body.Attributes["error_file"]; ok {
+								innerBlock.Body.Attributes["error_file"].Expr = absPath(attr)
+							}
+						} else if block.Type == backend {
+							absBackendBlock(innerBlock) // Backend block inside a AC block
+						}
+					}
+
+					if innerBlock.Type == backend {
+						absBackendBlock(innerBlock) // Backend block inside the definitions block
+					}
+				}
+			}
+		}
+	}
+
+	var blocks []*hclsyntax.Block
+
+	for _, labels := range definitionsBlock {
+		for _, block := range labels {
+			blocks = append(blocks, block)
+		}
+	}
+
+	return &hclsyntax.Block{
+		Type: definitions,
+		Body: &hclsyntax.Body{
+			Blocks: blocks,
+		},
+	}, nil
+}
+
+func mergeAttributes(blockName string, bodies []*hclsyntax.Body) *hclsyntax.Block {
+	attrs := make(hclsyntax.Attributes)
+
+	for _, body := range bodies {
+		for _, block := range body.Blocks {
+			if block.Type == blockName {
+				for name, attr := range block.Body.Attributes {
+					if blockName == settings && name == "ca_file" {
+						block.Body.Attributes[name].Expr = absPath(attr)
+					}
+
+					attrs[name] = attr
+				}
+			}
+		}
+	}
+
+	return &hclsyntax.Block{
+		Type: blockName,
+		Body: &hclsyntax.Body{
+			Attributes: attrs,
+		},
+	}
+}
diff --git a/config/configload/schema.go b/config/configload/schema.go
index 2b153872..e361c420 100644
--- a/config/configload/schema.go
+++ b/config/configload/schema.go
@@ -8,9 +8,11 @@ import (
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload/collect"
+	"github.com/avenga/couper/internal/seetie"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
 )
 
 const (
@@ -27,12 +29,12 @@ var (
 	reFetchUniqueKey       = regexp.MustCompile(`Key must be unique for (.*)\.`)
 )
 
-func ValidateConfigSchema(body hcl.Body, obj interface{}, src []byte) hcl.Diagnostics {
-	attrs, blocks, diags := getSchemaComponents(body, obj, src)
+func ValidateConfigSchema(body hcl.Body, obj interface{}) hcl.Diagnostics {
+	attrs, blocks, diags := getSchemaComponents(body, obj)
 	diags = filterValidErrors(attrs, blocks, diags)
 
 	for _, block := range blocks {
-		diags = diags.Extend(checkObjectFields(block, obj, src))
+		diags = diags.Extend(checkObjectFields(block, obj))
 	}
 
 	return uniqueErrors(diags)
@@ -92,7 +94,7 @@ func filterValidErrors(attrs hcl.Attributes, blocks hcl.Blocks, diags hcl.Diagno
 	return errors
 }
 
-func checkObjectFields(block *hcl.Block, obj interface{}, src []byte) hcl.Diagnostics {
+func checkObjectFields(block *hcl.Block, obj interface{}) hcl.Diagnostics {
 	var errors hcl.Diagnostics
 	var checked bool
 
@@ -111,7 +113,7 @@ func checkObjectFields(block *hcl.Block, obj interface{}, src []byte) hcl.Diagno
 
 		if field.Anonymous {
 			o := reflect.New(field.Type).Interface()
-			errors = errors.Extend(checkObjectFields(block, o, src))
+			errors = errors.Extend(checkObjectFields(block, o))
 
 			continue
 		}
@@ -130,7 +132,7 @@ func checkObjectFields(block *hcl.Block, obj interface{}, src []byte) hcl.Diagno
 
 		if field.Type.Kind() == reflect.Ptr {
 			o := reflect.New(field.Type.Elem()).Interface()
-			errors = errors.Extend(ValidateConfigSchema(block.Body, o, src))
+			errors = errors.Extend(ValidateConfigSchema(block.Body, o))
 
 			continue
 		} else if field.Type.Kind() == reflect.Slice {
@@ -161,7 +163,7 @@ func checkObjectFields(block *hcl.Block, obj interface{}, src []byte) hcl.Diagno
 				}
 
 				o := reflect.New(elem).Interface()
-				errors = errors.Extend(ValidateConfigSchema(block.Body, o, src))
+				errors = errors.Extend(ValidateConfigSchema(block.Body, o))
 
 				continue
 			}
@@ -175,14 +177,14 @@ func checkObjectFields(block *hcl.Block, obj interface{}, src []byte) hcl.Diagno
 
 	if !checked {
 		if i, ok := obj.(config.Inline); ok {
-			errors = errors.Extend(checkObjectFields(block, i.Inline(), src))
+			errors = errors.Extend(checkObjectFields(block, i.Inline()))
 		}
 	}
 
 	return errors
 }
 
-func getSchemaComponents(body hcl.Body, obj interface{}, src []byte) (hcl.Attributes, hcl.Blocks, hcl.Diagnostics) {
+func getSchemaComponents(body hcl.Body, obj interface{}) (hcl.Attributes, hcl.Blocks, hcl.Diagnostics) {
 	var (
 		attrs  = make(hcl.Attributes)
 		blocks hcl.Blocks
@@ -205,17 +207,17 @@ func getSchemaComponents(body hcl.Body, obj interface{}, src []byte) (hcl.Attrib
 		schema = config.WithErrorHandlerSchema(schema)
 	}
 
-	attrs, blocks, errors = completeSchemaComponents(body, schema, attrs, blocks, errors, src)
+	attrs, blocks, errors = completeSchemaComponents(body, schema, attrs, blocks, errors)
 
 	if i, ok := obj.(config.Inline); ok {
-		attrs, blocks, errors = completeSchemaComponents(body, i.Schema(true), attrs, blocks, errors, src)
+		attrs, blocks, errors = completeSchemaComponents(body, i.Schema(true), attrs, blocks, errors)
 	}
 
 	return attrs, blocks, errors
 }
 
 func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, attrs hcl.Attributes,
-	blocks hcl.Blocks, errors hcl.Diagnostics, src []byte) (hcl.Attributes, hcl.Blocks, hcl.Diagnostics) {
+	blocks hcl.Blocks, errors hcl.Diagnostics) (hcl.Attributes, hcl.Blocks, hcl.Diagnostics) {
 
 	content, diags := body.Content(schema)
 
@@ -245,27 +247,34 @@ func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, attrs hcl.A
 	if content != nil {
 		for name, attr := range content.Attributes {
 			if expr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr); ok {
-				unique := make(map[string]hcl.Range)
 
-				for _, item := range expr.Items {
-					keyRange := item.KeyExpr.Range()
-					if keyRange.CanSliceBytes(src) {
-						key := keyRange.SliceBytes(src)
-						lwrKey := strings.ToLower(string(key))
+				value, _ := attr.Expr.Value(nil)
+				if value.CanIterateElements() {
+					unique := make(map[string]struct{})
 
-						if previous, exist := unique[lwrKey]; exist {
+					iter := value.ElementIterator()
+
+					for {
+						if !iter.Next() {
+							break
+						}
+
+						k, _ := iter.Element()
+						if k.Type() != cty.String {
+							continue
+						}
+
+						keyName := strings.ToLower(seetie.ValueToString(k))
+						if _, ok := unique[keyName]; ok {
 							errors = errors.Append(&hcl.Diagnostic{
-								Subject:  &keyRange,
+								Subject:  &expr.SrcRange,
 								Severity: hcl.DiagError,
-								Summary: fmt.
-									Sprintf("key must be unique: '%s' was previously defined at: %s",
-										lwrKey,
-										previous.String()),
-								Detail: "Key must be unique for " + string(key) + ".",
+								Summary:  fmt.Sprintf("key in an attribute must be unique: '%s'", keyName),
+								Detail:   "Key must be unique for " + string(keyName) + ".",
 							})
 						}
 
-						unique[lwrKey] = keyRange
+						unique[keyName] = struct{}{}
 					}
 				}
 			}
diff --git a/config/couper.go b/config/couper.go
index 13f106a2..a613cb5f 100644
--- a/config/couper.go
+++ b/config/couper.go
@@ -9,9 +9,9 @@ const DefaultFilename = "couper.hcl"
 
 // Couper represents the <Couper> config object.
 type Couper struct {
-	Bytes       []byte
 	Context     context.Context
 	Filename    string
+	Dirpath     string
 	Definitions *Definitions `hcl:"definitions,block"`
 	Servers     Servers      `hcl:"server,block"`
 	Settings    *Settings    `hcl:"settings,block"`
diff --git a/docs/CLI.md b/docs/CLI.md
index 6dc05dac..67f40700 100644
--- a/docs/CLI.md
+++ b/docs/CLI.md
@@ -7,7 +7,7 @@
 Couper is build as binary called `couper` with the following commands:
 
 | Command   | Description                                                                                                                                   |
-| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
+|:----------|:----------------------------------------------------------------------------------------------------------------------------------------------|
 | `run`     | Start the server with given configuration file.                                                                                               |
 |           | _Note_: `run` options can also be configured with [settings](REFERENCE.md#settings-block) or related [environment variables](./../DOCKER.md). |
 | `help`    | Print the usage for the given command: `help run`                                                                                             |
@@ -19,6 +19,7 @@ Couper is build as binary called `couper` with the following commands:
 | Argument             | Default      | Environment                | Description                                                                                                                  |
 |:---------------------|:-------------|:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------|
 | `-f`                 | `couper.hcl` | `COUPER_FILE`              | File path to your Couper configuration file.                                                                                 |
+| `-d`                 | -            | `COUPER_FILE_DIRECTORY`    | Path to your Couper configuration files directory.                                                                           |
 | `-watch`             | `false`      | `COUPER_WATCH`             | Watch for configuration file changes and reload on modifications.                                                            |
 | `-watch-retries`     | `5`          | `COUPER_WATCH_RETRIES`     | Maximum retry count for configuration reloads which could not bind the configured port.                                      |
 | `-watch-retry-delay` | `500ms`      | `COUPER_WATCH_RETRY_DELAY` | Delay duration before next attempt if an error occurs.                                                                       |
@@ -29,12 +30,26 @@ Couper is build as binary called `couper` with the following commands:
 
 _Note_: `log-format`, `log-level` and `log-pretty` also map to [settings](REFERENCE.md#settings-block).
 
+_Note_: Couper can be started with both, `-f` and `-d` arguments. The path of `-f <file>`
+determines the working directory of Couper. If `-d <dir>` argument is given without the `-f <file>`,
+the path of `-d <dir>` is the working directory of Couper. A `couper.hcl` file inside the
+`-d <dir>` is priorized over other files inside the `-d <dir>`, but not over the
+`-f <file>`. Other files in the `-d <dir>` are loaded in alphabetical order. Example:
+
+```sh
+|- couper.hcl    # defined via `-f`                       <|
+|- couper.d/     # defined via `-d`                        |
+|  |- couper.hcl # patches the couper.hcl              <|  |
+|  |- a.hcl      # patches the couper.d/couper.hcl  <|  |
+|  |- z.hcl      # patches the couper.d/a.hcl        |
+```
+
 ## Run Options
 
-| Argument                | Default      | Environment                   | Description  |
-| :---------------------- | :----------- | :---------------------------- | :----------- |
+| Argument                | Default      | Environment                   | Description                                                                                                                                                                                                                           |
+|:------------------------|:-------------|:------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | `-accept-forwarded-url` | empty string | `COUPER_ACCEPT_FORWARDED_URL` | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](./REFERENCE.md#request) `url`, `origin`, `protocol`, `host`, `port`. Comma-separated list of values. Valid values: `proto`, `host`, `port` |
-| `-https-dev-proxy`      | empty string | `COUPER_HTTPS_DEV_PROXY`      | List of tls port mappings to define the tls listen port and the target one. A self-signed certificate will be generated on the fly based on given hostname. |
-| `-beta-metrics`         | -            | `COUPER_BETA_METRICS`         | Option to enable the prometheus [metrics](./METRICS.md) exporter. |
-| `-beta-metrics-port`    | `9090`       | `COUPER_BETA_METRICS_PORT`    | Prometheus exporter listen port. |
-| `-beta-service-name`    | `couper`     | `COUPER_BETA_SERVICE_NAME`    | The service name which applies to the `service_name` metric labels. |
+| `-https-dev-proxy`      | empty string | `COUPER_HTTPS_DEV_PROXY`      | List of tls port mappings to define the tls listen port and the target one. A self-signed certificate will be generated on the fly based on given hostname.                                                                           |
+| `-beta-metrics`         | -            | `COUPER_BETA_METRICS`         | Option to enable the prometheus [metrics](./METRICS.md) exporter.                                                                                                                                                                     |
+| `-beta-metrics-port`    | `9090`       | `COUPER_BETA_METRICS_PORT`    | Prometheus exporter listen port.                                                                                                                                                                                                      |
+| `-beta-service-name`    | `couper`     | `COUPER_BETA_SERVICE_NAME`    | The service name which applies to the `service_name` metric labels.                                                                                                                                                                   |
diff --git a/eval/context.go b/eval/context.go
index 8115f42e..f9d86d80 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -46,18 +46,17 @@ type Context struct {
 	oauth2            []config.OAuth2Authorization
 	jwtSigningConfigs map[string]*lib.JWTSigningConfig
 	saml              []*config.SAML
-	src               []byte
 	syncedVariables   *SyncedVariables
 }
 
-func NewContext(src []byte, defaults *config.Defaults) *Context {
+func NewContext(srcBytes [][]byte, defaults *config.Defaults) *Context {
 	var defaultEnvVariables config.DefaultEnvVars
 	if defaults != nil {
 		defaultEnvVariables = defaults.EnvironmentVariables
 	}
 
 	variables := make(map[string]cty.Value)
-	variables[Environment] = newCtyEnvMap(src, defaultEnvVariables)
+	variables[Environment] = newCtyEnvMap(srcBytes, defaultEnvVariables)
 	variables[Couper] = newCtyCouperVariablesMap()
 
 	return &Context{
@@ -66,7 +65,6 @@ func NewContext(src []byte, defaults *config.Defaults) *Context {
 			Functions: newFunctionsMap(),
 		},
 		inner: context.TODO(), // usually replaced with request context
-		src:   src,
 	}
 }
 
@@ -511,7 +509,7 @@ func newVariable(ctx context.Context, cookies []*http.Cookie, headers http.Heade
 	}
 }
 
-func newCtyEnvMap(src []byte, defaultValues map[string]string) cty.Value {
+func newCtyEnvMap(srcBytes [][]byte, defaultValues map[string]string) cty.Value {
 	ctyMap := make(map[string]cty.Value)
 	for k, v := range defaultValues {
 		ctyMap[k] = cty.StringVal(v)
@@ -535,10 +533,13 @@ func newCtyEnvMap(src []byte, defaultValues map[string]string) cty.Value {
 	}
 
 	emptyString := cty.StringVal("")
-	referenced := decodeEnvironmentRefs(src)
-	for _, key := range referenced {
-		if _, exist := ctyMap[key]; !exist {
-			ctyMap[key] = emptyString
+
+	for _, src := range srcBytes {
+		referenced := decodeEnvironmentRefs(src)
+		for _, key := range referenced {
+			if _, exist := ctyMap[key]; !exist {
+				ctyMap[key] = emptyString
+			}
 		}
 	}
 
diff --git a/main.go b/main.go
index c4a3c236..ad4aecff 100644
--- a/main.go
+++ b/main.go
@@ -8,11 +8,13 @@ import (
 	"context"
 	"flag"
 	"io"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/pprof"
 	"os"
 	"os/signal"
+	"strings"
 	"syscall"
 	"time"
 
@@ -49,6 +51,7 @@ func realmain(arguments []string) int {
 	type globalFlags struct {
 		DebugEndpoint       bool          `env:"debug"`
 		FilePath            string        `env:"file"`
+		DirPath             string        `env:"file_directory"`
 		FileWatch           bool          `env:"watch"`
 		FileWatchRetryDelay time.Duration `env:"watch_retry_delay"`
 		FileWatchRetries    int           `env:"watch_retries"`
@@ -60,7 +63,8 @@ func realmain(arguments []string) int {
 
 	set := flag.NewFlagSet("global options", flag.ContinueOnError)
 	set.BoolVar(&flags.DebugEndpoint, "debug", false, "-debug")
-	set.StringVar(&flags.FilePath, "f", config.DefaultFilename, "-f ./my-path/couper.hcl")
+	set.StringVar(&flags.FilePath, "f", "", "-f ./my-path/couper.hcl")
+	set.StringVar(&flags.DirPath, "d", "", "-d ./path/to/couper.d")
 	set.BoolVar(&flags.FileWatch, "watch", false, "-watch")
 	set.DurationVar(&flags.FileWatchRetryDelay, "watch-retry-delay", time.Millisecond*500, "-watch-retry-delay 1s")
 	set.IntVar(&flags.FileWatchRetries, "watch-retries", 5, "-watch-retries 10")
@@ -94,17 +98,21 @@ func realmain(arguments []string) int {
 	}
 	env.Decode(&flags)
 
+	if flags.DirPath == "" && flags.FilePath == "" {
+		flags.FilePath = config.DefaultFilename
+	}
+
 	if cmd == "verify" {
 		log := newLogger(flags.LogFormat, flags.LogLevel, flags.LogPretty)
 
-		err := command.NewCommand(ctx, cmd).Execute(command.Args{flags.FilePath}, nil, log)
+		err := command.NewCommand(ctx, cmd).Execute(command.Args{flags.FilePath, flags.DirPath}, nil, log)
 		if err != nil {
 			return 1
 		}
 		return 0
 	}
 
-	confFile, err := configload.LoadFile(flags.FilePath)
+	confFile, err := configload.LoadFiles(flags.FilePath, flags.DirPath)
 	if err != nil {
 		newLogger(flags.LogFormat, flags.LogLevel, flags.LogPretty).WithError(err).Error()
 		return 1
@@ -141,7 +149,7 @@ func realmain(arguments []string) int {
 	logger.WithField("watch", logrus.Fields{
 		"retry-delay": flags.FileWatchRetryDelay.String(),
 		"max-retries": flags.FileWatchRetries,
-	}).Info("watching configuration file")
+	}).Info("watching configuration file(s)")
 	errCh := make(chan error, 1)
 	errRetries := 0
 
@@ -157,7 +165,7 @@ func realmain(arguments []string) int {
 	sigCh := make(chan os.Signal, 1)
 	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
 
-	reloadCh := watchConfigFile(confFile.Filename, logger, flags.FileWatchRetries, flags.FileWatchRetryDelay)
+	reloadCh := watchConfigFiles(confFile.Filename, confFile.Dirpath, logger, flags.FileWatchRetries, flags.FileWatchRetryDelay)
 	for {
 		select {
 		case err = <-errCh:
@@ -194,7 +202,7 @@ func realmain(arguments []string) int {
 			errRetries = 0 // reset
 			logger.Info("reloading couper configuration")
 
-			cf, reloadErr := configload.LoadFile(confFile.Filename) // we are at wd, just filename
+			cf, reloadErr := configload.LoadFiles(confFile.Filename, confFile.Dirpath) // we are at wd, just filename
 			if reloadErr != nil {
 				logger.WithError(reloadErr).Error("reload failed")
 				time.Sleep(flags.FileWatchRetryDelay)
@@ -263,42 +271,136 @@ func newLogger(format, level string, pretty bool) *logrus.Entry {
 	return logger.WithField("type", logConf.TypeFieldKey).WithFields(fields)
 }
 
-func watchConfigFile(name string, logger logrus.FieldLogger, maxRetries int, retryDelay time.Duration) <-chan struct{} {
+func getWatchFilesList(filename, dirPath string) (map[string]struct{}, error) {
+	files := make(map[string]struct{})
+
+	if filename != "" {
+		files[filename] = struct{}{}
+	}
+	if dirPath != "" {
+		listing, err := ioutil.ReadDir(dirPath)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, file := range listing {
+			if file.IsDir() || !strings.HasSuffix(file.Name(), ".hcl") {
+				continue
+			}
+
+			files[dirPath+"/"+file.Name()] = struct{}{}
+		}
+	}
+
+	return files, nil
+}
+
+func retryWatching(errorsSeen, maxRetries int, retryDelay time.Duration,
+	logger logrus.FieldLogger, err error, reloadCh chan struct{}) bool {
+
+	if errorsSeen >= maxRetries {
+		logger.Errorf("giving up after %d retries: %v", errorsSeen, err)
+		close(reloadCh)
+
+		return false
+	}
+
+	logger.WithFields(fields).Error(err)
+
+	time.Sleep(retryDelay)
+
+	return true
+}
+
+func syncWatchFilesList(new map[string]struct{}, old map[string]time.Time) map[string]time.Time {
+	synced := make(map[string]time.Time)
+
+	for name := range new {
+		if t, ok := old[name]; ok {
+			synced[name] = t
+		} else {
+			synced[name] = time.Time{}
+		}
+	}
+
+	return synced
+}
+
+func watchConfigFiles(filename, dirPath string, logger logrus.FieldLogger, maxRetries int, retryDelay time.Duration) <-chan struct{} {
 	reloadCh := make(chan struct{})
+
 	go func() {
 		ticker := time.NewTicker(time.Second / 4)
 		defer ticker.Stop()
-		var lastChange time.Time
 		var errorsSeen int
+
+		lastChanges := make(map[string]time.Time)
+
+		files, err := getWatchFilesList(filename, dirPath)
+		if err != nil {
+			logger.Error(err)
+			close(reloadCh)
+			return
+		}
+
+		for name := range files {
+			lastChanges[name] = time.Time{}
+		}
+
+	watchFiles:
 		for {
 			<-ticker.C
-			fileInfo, fileErr := os.Stat(name)
-			if fileErr != nil {
+
+			// Compare files list
+			filesList, err := getWatchFilesList(filename, dirPath)
+			if err != nil {
 				errorsSeen++
-				if errorsSeen >= maxRetries {
-					logger.Errorf("giving up after %d retries: %v", errorsSeen, fileErr)
-					close(reloadCh)
+
+				if !retryWatching(errorsSeen, maxRetries, retryDelay, logger, err, reloadCh) {
 					return
 				}
 
-				logger.WithFields(fields).Error(fileErr)
-
-				time.Sleep(retryDelay)
-				continue
+				continue watchFiles
 			}
 
-			if lastChange.IsZero() { // first round
-				lastChange = fileInfo.ModTime()
-				continue
-			}
+			if len(filesList) != len(lastChanges) {
+				lastChanges = syncWatchFilesList(filesList, lastChanges)
 
-			if fileInfo.ModTime().After(lastChange) {
+				errorsSeen = 0
 				reloadCh <- struct{}{}
+
+				continue watchFiles
+			}
+
+			for f, t := range lastChanges {
+				info, err := os.Stat(f)
+
+				if err != nil {
+					errorsSeen++
+
+					if !retryWatching(errorsSeen, maxRetries, retryDelay, logger, err, reloadCh) {
+						return
+					}
+
+					continue watchFiles
+				}
+
+				if t.IsZero() { // first round
+					lastChanges[f] = info.ModTime()
+					continue watchFiles
+				}
+
+				if info.ModTime().After(t) {
+					reloadCh <- struct{}{}
+				}
+
+				lastChanges[f] = info.ModTime()
+
+				errorsSeen = 0
 			}
-			lastChange = fileInfo.ModTime()
-			errorsSeen = 0
 		}
 	}()
+
 	return reloadCh
 }
 


Test patch:
diff --git a/command/run_test.go b/command/run_test.go
index da4e5955..d778fa7c 100644
--- a/command/run_test.go
+++ b/command/run_test.go
@@ -103,7 +103,7 @@ func TestNewRun(t *testing.T) {
 				return
 			}
 
-			couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file))
+			couperFile, err := configload.LoadFiles(filepath.Join(wd, "testdata/settings", tt.file), "")
 			if err != nil {
 				subT.Error(err)
 			}
@@ -190,7 +190,7 @@ func TestAcceptForwarded(t *testing.T) {
 				return
 			}
 
-			couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file))
+			couperFile, err := configload.LoadFiles(filepath.Join(wd, "testdata/settings", tt.file), "")
 			if err != nil {
 				subT.Error(err)
 			}
diff --git a/config/configload/error_handler_test.go b/config/configload/error_handler_test.go
index cc515045..6d340834 100644
--- a/config/configload/error_handler_test.go
+++ b/config/configload/error_handler_test.go
@@ -12,7 +12,7 @@ func TestErrorHandler_newKindsFromLabels(t *testing.T) {
 	b := &hcl.Block{
 		Labels: []string{""},
 		LabelRanges: []hcl.Range{
-			hcl.Range{
+			{
 				Start: hcl.Pos{
 					Line:   123,
 					Column: 321,
diff --git a/main_test.go b/main_test.go
index 1d91a1e4..36a8a357 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"fmt"
 	"os"
+	"regexp"
 	"strings"
 	"testing"
 
@@ -26,14 +28,14 @@ func Test_realmain(t *testing.T) {
 	}{
 		{"verify", []string{"couper", "verify", "-f", base + "/10_couper.hcl"}, nil, `10_couper.hcl:2,3-6: Unsupported block type; Blocks of type \"foo\" are not expected here.`, 1},
 		{"verify w/o server", []string{"couper", "verify", "-f", base + "/11_couper.hcl"}, nil, `configuration error: missing 'server' block"`, 1},
-		{"verify unique map-attr keys", []string{"couper", "verify", "-f", base + "/12_couper.hcl"}, nil, `12_couper.hcl:7,7-15: key must be unique: 'test-key' was previously defined at: 12_couper.hcl:6,7-15;`, 1},
-		{"common log format & info log level /wo file", []string{"couper", "run"}, nil, `level=error msg="failed to load configuration: open couper.hcl: no such file or directory" build=dev`, 1},
-		{"common log format via env /wo file", []string{"couper", "run", "-log-format", "json"}, []string{"COUPER_LOG_FORMAT=common"}, `level=error msg="failed to load configuration: open couper.hcl: no such file or directory" build=dev`, 1},
-		{"info log level via env /wo file", []string{"couper", "run", "-log-level", "debug"}, []string{"COUPER_LOG_LEVEL=info"}, `level=error msg="failed to load configuration: open couper.hcl: no such file or directory" build=dev`, 1},
-		{"json log format /wo file", []string{"couper", "run", "-log-format", "json"}, nil, `{"build":"dev","level":"error","message":"failed to load configuration: open couper.hcl: no such file or directory"`, 1},
-		{"json log format via env /wo file", []string{"couper", "run"}, []string{"COUPER_LOG_FORMAT=json"}, `{"build":"dev","level":"error","message":"failed to load configuration: open couper.hcl: no such file or directory"`, 1},
-		{"non-existent log level /wo file", []string{"couper", "run", "-log-level", "test"}, nil, `level=error msg="failed to load configuration: open couper.hcl: no such file or directory" build=dev`, 1},
-		{"non-existent log level via env /wo file", []string{"couper", "run"}, []string{"COUPER_LOG_LEVEL=test"}, `level=error msg="failed to load configuration: open couper.hcl: no such file or directory" build=dev`, 1},
+		{"verify unique map-attr keys", []string{"couper", "verify", "-f", base + "/12_couper.hcl"}, nil, `12_couper.hcl:5,28-8,6: key in an attribute must be unique: 'test-key'; Key must be unique for test-key.`, 1},
+		{"common log format & info log level /wo file", []string{"couper", "run"}, nil, `level=error msg="failed to load configuration: open .*/couper.hcl: no such file or directory" build=dev`, 1},
+		{"common log format via env /wo file", []string{"couper", "run", "-log-format", "json"}, []string{"COUPER_LOG_FORMAT=common"}, `level=error msg="failed to load configuration: open .*/couper.hcl: no such file or directory" build=dev`, 1},
+		{"info log level via env /wo file", []string{"couper", "run", "-log-level", "debug"}, []string{"COUPER_LOG_LEVEL=info"}, `level=error msg="failed to load configuration: open .*/couper.hcl: no such file or directory" build=dev`, 1},
+		{"json log format /wo file", []string{"couper", "run", "-log-format", "json"}, nil, `{"build":"dev","level":"error","message":"failed to load configuration: open .*/couper.hcl: no such file or directory"`, 1},
+		{"json log format via env /wo file", []string{"couper", "run"}, []string{"COUPER_LOG_FORMAT=json"}, `{"build":"dev","level":"error","message":"failed to load configuration: open .*/couper.hcl: no such file or directory"`, 1},
+		{"non-existent log level /wo file", []string{"couper", "run", "-log-level", "test"}, nil, `level=error msg="failed to load configuration: open .*/couper.hcl: no such file or directory" build=dev`, 1},
+		{"non-existent log level via env /wo file", []string{"couper", "run"}, []string{"COUPER_LOG_LEVEL=test"}, `level=error msg="failed to load configuration: open .*/couper.hcl: no such file or directory" build=dev`, 1},
 		{"common log format & info log level /w file", []string{"couper", "run", "-f", base + "/log_default.hcl"}, nil, `level=error msg="configuration error: missing 'server' block" build=dev`, 1},
 		{"common log format via env /w file", []string{"couper", "run", "-f", base + "/log_altered.hcl"}, []string{"COUPER_LOG_FORMAT=common"}, `level=error msg="configuration error: missing 'server' block" build=dev`, 1},
 		{"info log level via env /w file", []string{"couper", "run", "-f", base + "/log_default.hcl"}, []string{"COUPER_LOG_LEVEL=info"}, `level=error msg="configuration error: missing 'server' block" build=dev`, 1},
@@ -42,9 +44,9 @@ func Test_realmain(t *testing.T) {
 		{"non-existent log level via env /w file", []string{"couper", "run", "-f", base + "/log_altered.hcl"}, []string{"COUPER_LOG_LEVEL=test"}, `level=error msg="configuration error: missing 'server' block" build=dev`, 1},
 		{"-f w/o file", []string{"couper", "run", "-f"}, nil, `level=error msg="flag needs an argument: -f" build=dev`, 1},
 		{"undefined AC", []string{"couper", "run", "-f", base + "/04_couper.hcl"}, nil, `level=error msg="accessControl is not defined: undefined" build=dev`, 1},
-		{"empty string in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/13_couper.hcl"}, nil, `level=error msg="13_couper.hcl:3,5-27: method contains invalid character(s); " build=dev`, 1},
-		{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
-		{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
+		{"empty string in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/13_couper.hcl"}, nil, `level=error msg="%s/13_couper.hcl:3,5-27: method contains invalid character(s); " build=dev`, 1},
+		{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="%s/14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
+		{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="%s/15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
@@ -59,10 +61,26 @@ func Test_realmain(t *testing.T) {
 			}
 			env.OsEnviron = os.Environ
 
+			currWD, _ := os.Getwd()
+
 			entry, _ := localHook.LastEntry().String()
 			//println(entry)
-			if tt.wantLog != "" && !strings.Contains(entry, tt.wantLog) {
-				subT.Errorf("\nwant:\t%s\ngot:\t%s\n", tt.wantLog, entry)
+
+			wantLog := tt.wantLog
+			if strings.Contains(wantLog, `msg="%s/`) {
+				wantLog = fmt.Sprintf(wantLog, currWD)
+			}
+
+			if wantLog != "" && !strings.Contains(entry, wantLog) {
+				if strings.Contains(wantLog, `failed to load configuration:`) {
+					re := regexp.MustCompile(wantLog)
+
+					if !re.MatchString(entry) {
+						subT.Errorf("\nwant:\t%s\ngot:\t%s\n", wantLog, entry)
+					}
+				} else {
+					subT.Errorf("\nwant:\t%s\ngot:\t%s\n", wantLog, entry)
+				}
 			}
 
 			err := os.Chdir(wd)
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index c83cebdb..da9acde8 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -811,7 +811,7 @@ func TestEndpointCyclicSequence(t *testing.T) {
 			defer cleanup(func() {}, test.New(t))
 
 			path := filepath.Join(testdataPath, testcase.file)
-			_, err := configload.LoadFile(path)
+			_, err := configload.LoadFiles(path, "")
 
 			diags, ok := err.(*hcl.Diagnostic)
 			if !ok {
@@ -832,11 +832,6 @@ func TestEndpointErrorHandler(t *testing.T) {
 
 	shutdown, hook := newCouper(filepath.Join(testdataPath, "14_couper.hcl"), helper)
 	defer shutdown()
-	defer func() {
-		for _, e := range hook.AllEntries() {
-			t.Logf("%#v", e.Data)
-		}
-	}()
 
 	type testcase struct {
 		name              string
diff --git a/server/http_error_handler_test.go b/server/http_error_handler_test.go
index 638467d7..cfeb965c 100644
--- a/server/http_error_handler_test.go
+++ b/server/http_error_handler_test.go
@@ -3,6 +3,7 @@ package server_test
 import (
 	"context"
 	"net/http"
+	"strings"
 	"testing"
 
 	logrustest "github.com/sirupsen/logrus/hooks/test"
@@ -115,7 +116,7 @@ func TestAccessControl_ErrorHandler_BasicAuth_Wildcard(t *testing.T) {
 
 func TestAccessControl_ErrorHandler_Configuration_Error(t *testing.T) {
 	helper := test.New(t)
-	couperConfig, err := configload.LoadFile("testdata/integration/error_handler/03_couper.hcl")
+	couperConfig, err := configload.LoadFiles("testdata/integration/error_handler/03_couper.hcl", "")
 	helper.Must(err)
 
 	log, _ := logrustest.NewNullLogger()
@@ -126,7 +127,7 @@ func TestAccessControl_ErrorHandler_Configuration_Error(t *testing.T) {
 	err = command.NewRun(ctx).Execute([]string{couperConfig.Filename}, couperConfig, log.WithContext(ctx))
 	if err == nil {
 		t.Error("logErr should not be nil")
-	} else if err.Error() != expectedMsg {
+	} else if !strings.HasSuffix(err.Error(), expectedMsg) {
 		t.Errorf("\nwant:\t%s\ngot:\t%v", expectedMsg, err.Error())
 	}
 }
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 95c1fef6..4cde3812 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -83,8 +83,16 @@ func teardown() {
 	}
 	testBackend.Close()
 }
+
 func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
-	couperConfig, err := configload.LoadFile(filepath.Join(testWorkingDir, file))
+	couperConfig, err := configload.LoadFiles(filepath.Join(testWorkingDir, file), "")
+	helper.Must(err)
+
+	return newCouperWithConfig(couperConfig, helper)
+}
+
+func newCouperMultiFiles(file, dir string, helper *test.Helper) (func(), *logrustest.Hook) {
+	couperConfig, err := configload.LoadFiles(filepath.Join(testWorkingDir, file), dir)
 	helper.Must(err)
 
 	return newCouperWithConfig(couperConfig, helper)
@@ -3281,7 +3289,7 @@ func TestJWTAccessControl(t *testing.T) {
 
 func TestJWTAccessControlSourceConfig(t *testing.T) {
 	helper := test.New(t)
-	couperConfig, err := configload.LoadFile("testdata/integration/config/05_couper.hcl")
+	couperConfig, err := configload.LoadFiles("testdata/integration/config/05_couper.hcl", "")
 	helper.Must(err)
 
 	log, _ := logrustest.NewNullLogger()
@@ -3956,6 +3964,11 @@ func TestFunction_to_number_errors(t *testing.T) {
 	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
 	defer shutdown()
 
+	wd, werr := os.Getwd()
+	if werr != nil {
+		t.Fatal(werr)
+	}
+
 	type testCase struct {
 		name   string
 		path   string
@@ -3963,10 +3976,10 @@ func TestFunction_to_number_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"string", "/v1/to_number/string", `expression evaluation error: 01_couper.hcl:62,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`},
-		{"bool", "/v1/to_number/bool", `expression evaluation error: 01_couper.hcl:70,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`},
-		{"tuple", "/v1/to_number/tuple", `expression evaluation error: 01_couper.hcl:78,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`},
-		{"object", "/v1/to_number/object", `expression evaluation error: 01_couper.hcl:86,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`},
+		{"string", "/v1/to_number/string", `expression evaluation error: ` + wd + `/01_couper.hcl:62,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`},
+		{"bool", "/v1/to_number/bool", `expression evaluation error: ` + wd + `/01_couper.hcl:70,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`},
+		{"tuple", "/v1/to_number/tuple", `expression evaluation error: ` + wd + `/01_couper.hcl:78,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`},
+		{"object", "/v1/to_number/object", `expression evaluation error: ` + wd + `/01_couper.hcl:86,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -3994,6 +4007,11 @@ func TestFunction_length_errors(t *testing.T) {
 	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
 	defer shutdown()
 
+	wd, werr := os.Getwd()
+	if werr != nil {
+		t.Fatal(werr)
+	}
+
 	type testCase struct {
 		name   string
 		path   string
@@ -4001,9 +4019,9 @@ func TestFunction_length_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"object", "/v1/length/object", `expression evaluation error: 01_couper.hcl:123,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
-		{"string", "/v1/length/string", `expression evaluation error: 01_couper.hcl:131,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
-		{"null", "/v1/length/null", `expression evaluation error: 01_couper.hcl:139,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`},
+		{"object", "/v1/length/object", `expression evaluation error: ` + wd + `/01_couper.hcl:123,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
+		{"string", "/v1/length/string", `expression evaluation error: ` + wd + `/01_couper.hcl:131,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
+		{"null", "/v1/length/null", `expression evaluation error: ` + wd + `/01_couper.hcl:139,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -4031,6 +4049,11 @@ func TestFunction_lookup_errors(t *testing.T) {
 	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
 	defer shutdown()
 
+	wd, werr := os.Getwd()
+	if werr != nil {
+		t.Fatal(werr)
+	}
+
 	type testCase struct {
 		name   string
 		path   string
@@ -4038,7 +4061,7 @@ func TestFunction_lookup_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"null inputMap", "/v1/lookup/inputMap-null", `expression evaluation error: 01_couper.hcl:200,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`},
+		{"null inputMap", "/v1/lookup/inputMap-null", `expression evaluation error: ` + wd + `/01_couper.hcl:200,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -4982,13 +5005,13 @@ func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"/conditional/null", "expression evaluation error: 20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."},
-		{"/conditional/string", "expression evaluation error: 20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/number", "expression evaluation error: 20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/tuple", "expression evaluation error: 20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/object", "expression evaluation error: 20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/string/expr", "expression evaluation error: 20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/number/expr", "expression evaluation error: 20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/null", "20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."},
+		{"/conditional/string", "20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/number", "20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/tuple", "20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/object", "20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/string/expr", "20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/number/expr", "20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -5018,7 +5041,10 @@ func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
 			time.Sleep(time.Millisecond * 100)
 			entry := hook.LastEntry()
 			if entry != nil && entry.Level == logrus.ErrorLevel {
-				if entry.Message != tc.expMessage {
+				if !strings.HasPrefix(entry.Message, "expression evaluation error: ") {
+					subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message)
+				}
+				if !strings.HasSuffix(entry.Message, tc.expMessage) {
 					subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message)
 				}
 			}
diff --git a/server/multi_files_test.go b/server/multi_files_test.go
new file mode 100644
index 00000000..4332da3e
--- /dev/null
+++ b/server/multi_files_test.go
@@ -0,0 +1,160 @@
+package server_test
+
+import (
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/avenga/couper/internal/test"
+)
+
+func TestMultiFiles_Server(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, hook := newCouperMultiFiles(
+		"testdata/multi/server/couper.hcl",
+		"testdata/multi/server/couper.d",
+		helper,
+	)
+	defer shutdown()
+
+	for _, entry := range hook.AllEntries() {
+		t.Log(entry.String())
+	}
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(req)
+	if err == nil || err.Error() != `Get "http://example.com:8080/": dial tcp4 127.0.0.1:8080: connect: connection refused` {
+		t.Error("Expected hosts port override to 9080")
+	}
+
+	type testcase struct {
+		url       string
+		expStatus int
+		expBody   string
+	}
+
+	for _, tc := range []testcase{
+		{"http://example.com:9080/", http.StatusOK, "<body>RIGHT INCLUDE</body>\n"},
+		{"http://example.com:9080/free", http.StatusForbidden, ""},
+		{"http://example.com:9080/api-111", http.StatusUnauthorized, ""},
+		{"http://example.com:9080/api-3", http.StatusTeapot, ""},
+		{"http://example.com:9080/api-4/ep", http.StatusNoContent, ""},
+		{"http://example.com:9081/", http.StatusOK, ""},
+		{"http://example.com:8082/", http.StatusOK, ""},
+		{"http://example.com:8083/", http.StatusNotFound, ""},
+		{"http://example.com:9084/", http.StatusNotFound, ""},
+	} {
+		req, err = http.NewRequest(http.MethodGet, tc.url, nil)
+		helper.Must(err)
+
+		res, err = client.Do(req)
+		helper.Must(err)
+
+		if res.StatusCode != tc.expStatus {
+			t.Errorf("request %q: want status %d, got %d", tc.url, tc.expStatus, res.StatusCode)
+		}
+
+		if tc.expBody == "" {
+			continue
+		}
+
+		resBytes, err := io.ReadAll(res.Body)
+		helper.Must(err)
+		_ = res.Body.Close()
+
+		if string(resBytes) != tc.expBody {
+			t.Errorf("request %q unexpected content: %s", tc.url, resBytes)
+		}
+	}
+}
+
+func TestMultiFiles_SettingsAndDefaults(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, _ := newCouperMultiFiles(
+		"testdata/multi/settings/couper.hcl",
+		"testdata/multi/settings/couper.d",
+		helper,
+	)
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	resBytes, err := io.ReadAll(res.Body)
+	helper.Must(err)
+	_ = res.Body.Close()
+
+	if !strings.Contains(string(resBytes), `"Req-Id-Be-Hdr":["`) {
+		t.Errorf("%s", resBytes)
+	}
+
+	if res.Header.Get("Req-Id-Cl-Hdr") == "" {
+		t.Errorf("Missing 'Req-Id-Cl-Hdr' header")
+	}
+
+	// Call health route
+	req, err = http.NewRequest(http.MethodGet, "http://example.com:8080/xyz", nil)
+	helper.Must(err)
+
+	res, err = client.Do(req)
+	helper.Must(err)
+
+	resBytes, err = io.ReadAll(res.Body)
+	helper.Must(err)
+	_ = res.Body.Close()
+
+	if s := string(resBytes); s != "healthy" {
+		t.Errorf("Unexpected body given: %s", s)
+	}
+}
+
+func TestMultiFiles_Definitions(t *testing.T) {
+	helper := test.New(t)
+	client := newClient()
+
+	shutdown, _ := newCouperMultiFiles(
+		"testdata/multi/definitions/couper.hcl",
+		"testdata/multi/definitions/couper.d",
+		helper,
+	)
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
+	helper.Must(err)
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	resBytes, err := io.ReadAll(res.Body)
+	helper.Must(err)
+	_ = res.Body.Close()
+
+	if s := string(resBytes); s != "1234567890" {
+		t.Errorf("Unexpected body given: %s", s)
+	}
+
+	// Call protected route
+	req, err = http.NewRequest(http.MethodGet, "http://example.com:8080/added", nil)
+	helper.Must(err)
+
+	res, err = client.Do(req)
+	helper.Must(err)
+
+	resBytes, err = io.ReadAll(res.Body)
+	helper.Must(err)
+	_ = res.Body.Close()
+
+	if s := string(resBytes); !strings.Contains(s, "401 access control error") {
+		t.Errorf("Unexpected body given: %s", s)
+	}
+}
diff --git a/server/testdata/multi/definitions/couper.d/aaa.hcl b/server/testdata/multi/definitions/couper.d/aaa.hcl
new file mode 100644
index 00000000..8742dbf2
--- /dev/null
+++ b/server/testdata/multi/definitions/couper.d/aaa.hcl
@@ -0,0 +1,21 @@
+server {
+  endpoint "/added" {
+    access_control = ["BA"]
+
+    proxy {
+      backend = "Added"
+    }
+  }
+}
+
+definitions {
+  backend "Backend" {
+    origin = "${env.COUPER_TEST_BACKEND_ADDR}"
+    path = "/small"
+  }
+
+  basic_auth "BA" {
+    user     = "User"
+    password = "Pass"
+  }
+}
diff --git a/server/testdata/multi/definitions/couper.d/couper.hcl b/server/testdata/multi/definitions/couper.d/couper.hcl
new file mode 100644
index 00000000..6d1606a3
--- /dev/null
+++ b/server/testdata/multi/definitions/couper.d/couper.hcl
@@ -0,0 +1,15 @@
+definitions {
+  backend "Backend" {
+    origin = "https://example.org"
+  }
+
+  backend "Added" {
+    origin = "${env.COUPER_TEST_BACKEND_ADDR}"
+    path = "/small"
+  }
+
+  basic_auth "BA" {
+    user     = "USR"
+    password = "PWD"
+  }
+}
diff --git a/server/testdata/multi/definitions/couper.hcl b/server/testdata/multi/definitions/couper.hcl
new file mode 100644
index 00000000..23c182d9
--- /dev/null
+++ b/server/testdata/multi/definitions/couper.hcl
@@ -0,0 +1,19 @@
+server {
+  endpoint "/" {
+    proxy {
+      backend = "Backend"
+    }
+  }
+}
+
+definitions {
+  backend "Backend" {
+    origin = "${env.COUPER_TEST_BACKEND_ADDR}"
+    path = "/anything"
+  }
+
+  basic_auth "BA" {
+    user     = "U"
+    password = "P"
+  }
+}
diff --git a/server/testdata/multi/server/couper.d/aaa.hcl b/server/testdata/multi/server/couper.d/aaa.hcl
new file mode 100644
index 00000000..f0276b40
--- /dev/null
+++ b/server/testdata/multi/server/couper.d/aaa.hcl
@@ -0,0 +1,14 @@
+server {
+  files {
+    document_root = "./www"
+    custom_log_fields = {
+      from = "final"
+    }
+  }
+
+  endpoint "/free" {
+    response {
+      status = 403
+    }
+  }
+}
diff --git a/server/testdata/multi/server/couper.d/couper.hcl b/server/testdata/multi/server/couper.d/couper.hcl
new file mode 100644
index 00000000..7983e576
--- /dev/null
+++ b/server/testdata/multi/server/couper.d/couper.hcl
@@ -0,0 +1,86 @@
+server {
+  hosts = ["*:9080"]
+
+  cors {
+    allowed_origins = ["origin-2"]
+  }
+
+  files {
+    document_root = "./web"
+    custom_log_fields = {
+      from = "override"
+    }
+  }
+
+  endpoint "/free" {
+    response {
+      status = 401
+    }
+  }
+
+  api "API" {
+    base_path      = "/api-111"
+    access_control = ["foo"]
+
+    error_handler "beta_insufficient_scope" {
+      response {
+        status = 415
+      }
+    }
+
+    endpoint "/" {
+      request "r" {
+        url = "https://couper.io/"
+      }
+
+      response {
+        status = 204
+      }
+    }
+
+    endpoint "/other" {
+      response {
+        status = 200
+      }
+    }
+  }
+
+  api {
+    base_path = "/api-3"
+
+    endpoint "/" {
+      response {
+        status = 418
+      }
+    }
+  }
+
+  api "NewAPI" {
+    base_path = "/api-4"
+    endpoint "/ep" {
+      response {
+        status = 204
+      }
+    }
+  }
+}
+
+server "ServerA" {
+  hosts = ["*:9081"]
+}
+
+server "ServerB" {
+  cors {
+    allowed_origins = ["origin-b"]
+  }
+}
+
+server "ServerD" {
+  hosts = ["*:9084"]
+}
+
+definitions {
+  basic_auth "foo" {
+    password = "abc"
+  }
+}
diff --git a/server/testdata/multi/server/couper.d/www/index.html b/server/testdata/multi/server/couper.d/www/index.html
new file mode 100644
index 00000000..c59cd116
--- /dev/null
+++ b/server/testdata/multi/server/couper.d/www/index.html
@@ -0,0 +1,1 @@
+<body>RIGHT INCLUDE</body>
diff --git a/server/testdata/multi/server/couper.hcl b/server/testdata/multi/server/couper.hcl
new file mode 100644
index 00000000..90a0ad66
--- /dev/null
+++ b/server/testdata/multi/server/couper.hcl
@@ -0,0 +1,79 @@
+server {
+  hosts = ["*:8080"]
+
+  cors {
+    allowed_origins = ["origin-1"]
+    allow_credentials = true
+  }
+
+  files {
+    document_root = "./htdocs"
+    custom_log_fields = {
+      from = "base"
+    }
+  }
+
+  endpoint "/free" {
+    response {
+      status = 400
+    }
+  }
+
+  api "API" {
+    base_path = "/api-1"
+
+    error_handler "beta_insufficient_scope" {
+      response {
+        status = 401
+      }
+    }
+
+    endpoint "/" {
+      proxy {
+        url = "https://example.com"
+      }
+    }
+  }
+
+  api {
+    base_path = "/api-2"
+
+    error_handler {
+      response {
+        status = 401
+      }
+    }
+  }
+}
+
+server "ServerA" {
+  hosts = ["*:8081"]
+
+  cors {
+    allowed_origins = ["origin-a"]
+  }
+
+  api {
+    endpoint "/" {
+      response {
+        body = "OK"
+      }
+    }
+  }
+}
+
+server "ServerB" {
+  hosts = ["*:8082"]
+
+  api {
+    endpoint "/" {
+      response {
+        body = "OK"
+      }
+    }
+  }
+}
+
+server "ServerC" {
+  hosts = ["*:8083"]
+}
diff --git a/server/testdata/multi/settings/couper.d/aaa.hcl b/server/testdata/multi/settings/couper.d/aaa.hcl
new file mode 100644
index 00000000..056dc99b
--- /dev/null
+++ b/server/testdata/multi/settings/couper.d/aaa.hcl
@@ -0,0 +1,14 @@
+defaults {
+  environment_variables = {
+    ZZZ = "/xyz"
+  }
+}
+
+settings {
+  health_path = env.ZZZ
+
+  request_id_client_header  = "Req-Id-Cl-Hdr"
+  request_id_backend_header = "Req-Id-Be-Hdr"
+
+  ca_file = "../../../integration/files/certificate.pem"
+}
diff --git a/server/testdata/multi/settings/couper.d/couper.hcl b/server/testdata/multi/settings/couper.d/couper.hcl
new file mode 100644
index 00000000..ba5aed34
--- /dev/null
+++ b/server/testdata/multi/settings/couper.d/couper.hcl
@@ -0,0 +1,10 @@
+defaults {
+  environment_variables = {
+    Y = "Y"
+  }
+}
+
+settings {
+  health_path = "/def"
+  request_id_backend_header = "R-I-B-H"
+}
diff --git a/server/testdata/multi/settings/couper.hcl b/server/testdata/multi/settings/couper.hcl
new file mode 100644
index 00000000..69944046
--- /dev/null
+++ b/server/testdata/multi/settings/couper.hcl
@@ -0,0 +1,18 @@
+server {
+  endpoint "/" {
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
+    }
+  }
+}
+
+defaults {
+  environment_variables = {
+    X = "X"
+  }
+}
+
+settings {
+  health_path = "/abc"
+  ca_file = "../../integration/files/not-there.pem"
+}
diff --git a/server/testdata/settings/12_couper.hcl b/server/testdata/settings/12_couper.hcl
index 374c8694..ae2733bc 100644
--- a/server/testdata/settings/12_couper.hcl
+++ b/server/testdata/settings/12_couper.hcl
@@ -4,7 +4,7 @@ server {
 
     set_response_headers = {
       test-key = "value"
-      test-key = "value"
+      Test-Key = "value"
     }
   }
 }