Map #

The map directive is one of the most advanced declarative programming features offered by the Caddy web server. This directive lets you map values from input variables or Caddy placeholders (like the IP address {remote_host}, request headers {header.Origin}, or URL paths {path}) to one or more new output variables using a lookup table you define. The mapping result is then stored in custom variables you can call in subsequent Caddyfile configuration sections using standard placeholder syntax (like {variable_name}). By using map, you can avoid writing hundreds of lines of complicated, error-prone conditional branching logic (if/else or nested matchers), keeping your Caddyfile clean, modular, and very easy to maintain at production scale. We’ll discuss in depth how the map directive works, practice mapping multiple variables at once (multiple outputs), apply regular expression (regex) matching patterns, and explore advanced use cases like dynamic backend cluster routing, maintenance mode management, and dynamic security header configuration.


Basic Concepts and Syntax of the map Directive #

Before using the map directive, you must understand the philosophy behind its existence. On traditional web servers, to map routes based on certain conditions — for example, routing traffic to different backend servers depending on the tenant subdomain — you usually have to write many server blocks or dozens of nested matcher rules:

// ANTI-PATTERN: Writing repetitive manual matchers for every route condition
// This way is terrible because it complicates adding new tenants in the future
@tenant_acme host acme.example.com
reverse_proxy @tenant_acme acme-service:8080

@tenant_globex host globex.example.com
reverse_proxy @tenant_globex globex-service:8080

With the map directive, you separate routing logic from mapping data. You define one central lookup table mapping domains to backend addresses, then call that address dynamically.

The basic syntax of the map directive is:

map <input> <outputs...> {
    <input_value> <output_values...>
    default       <default_output_values...>
}
  • <input>: The input data source to evaluate. Usually a Caddy placeholder like {host}, {path}, or {header.X-API-Version}.
  • <outputs...>: One or more new custom variable names wrapped in curly braces, which will hold the mapping result.
  • default: The fallback value that gets filled into the output variables if the client input doesn’t match any rule in the lookup table.

Basic Implementation Example #

# Example: Dynamic Backend Routing Based on API Version
api.example.com {
    # 1. Define the mapping from the X-API-Version header to the backend address
    map {header.X-API-Version} {backend_address} {
        "v1"    "api-v1-service:8081"
        "v2"    "api-v2-service:8082"
        "v3"    "api-v3-service:8083"
        
        # Provide a default value if the client doesn't send the header
        default "api-v2-service:8082"
    }

    # 2. Call the mapping result variable in reverse_proxy
    # Caddy evaluates {backend_address} dynamically for every request
    reverse_proxy {backend_address}
}

Mapping Multiple Output Variables at Once #

One of the biggest advantages of Caddy’s map directive is its ability to produce several output variables in parallel from a single input evaluation. This is very useful when you want to configure multiple interrelated response settings.

For example, you want to detect the user’s language preference from the Accept-Language header and simultaneously determine the HTML tag language code (lang) plus the page’s text writing direction (dir - left-to-right or right-to-left):

# Example: Dynamic Internationalization with Multiple Outputs
example.com {
    # Map the Accept-Language header to two output variables at once
    map {header.Accept-Language} {client_language} {text_direction} {
        # We use the tilde prefix (~) to enable regex matching
        ~^id    "id"  "ltr"    # Indonesian -> Left to Right
        ~^ar    "ar"  "rtl"    # Arabic -> Right to Left
        ~^he    "he"  "rtl"    # Hebrew -> Right to Left
        ~^ja    "ja"  "ltr"    # Japanese -> Left to Right
        
        # Fallback if no language matches
        default "en"  "ltr"    # English -> Left to Right
    }

    # Use the mapping result variables to compose HTTP response headers
    header Content-Language {client_language}
    header X-Text-Direction {text_direction}

    reverse_proxy localhost:8080
}

In the example above, when Caddy evaluates an Accept-Language header value of ar-EG (Egyptian Arabic), Caddy matches the ~^ar regex expression and immediately inserts the value "ar" into the {client_language} variable and "rtl" into the {text_direction} variable in one very fast computation.


Lookup Table Matching Rules: Static and Regular Expressions #

Caddy provides high flexibility in how lookup tables match client input values. There are three main matching methods:

1. Exact Static Matching #

The input value is matched exactly (case-sensitive). This is the fastest built-in method because it uses direct string comparison.

"dashboard.example.com" "backend-dashboard:3000"

2. Regular Expression Matching (Regex) #

If the lookup value starts with a tilde character (~), Caddy treats the rest of the string as a regular expression.

~(?i)mobile|android|iphone  "mobile-content"

Capturing groups in regex can’t be used directly in the map’s output columns, but you can use regex matching to map categories.

3. Sub-domain Wildcard Matching #

Caddy allows using an asterisk (*) character at the start of a lookup value to match subdomains dynamically:

*.example.com "subdomain-backend:8080"

map Directive Evaluation Workflow Diagram #

To understand how Caddy evaluates the lookup table from the incoming request to determining the route, let’s look at the following flowchart:

flowchart TD
    A["1. HTTP Request Arrives at Caddy"] --> B["2. Caddy reads the input variable value\n(e.g., {header.Origin} or {host})"]
    B --> C{"3. Is the input empty?"}
    
    C -- "Yes" --> D["4. Use the Default Value\n(Fallback rule)"]
    C -- "No" --> E{"5. Does it match a Lookup Table row?\n(Exact static matching)"}
    
    E -- "Yes" --> F["6. Take the output values defined on that row"]
    E -- "No" --> G{"7. Does it start with a tilde (~)?\n(Regex evaluation)"}
    
    G -- "Yes" --> H{"Does the regex pattern match?"}
    G -- "No" --> D
    
    H -- "Yes" --> F
    H -- "No" --> D
    
    F --> I["8. Store the values into custom variables\n(e.g. {backend_addr})"]
    D --> I
    
    I --> J["9. Use the custom variables in subsequent Caddy directives\n(e.g. reverse_proxy {backend_addr})"]

External File Integration for Large-Scale SaaS #

When you run a large-scale multi-tenant Software-as-a-Service (SaaS) with thousands of custom domain customers, writing all the mapping rows directly in the main Caddyfile makes it very long and hard to manage collaboratively.

Caddy solves this problem by allowing you to import mapping rows from isolated external text files. These external files can be updated automatically using automation scripts or your backend database administration system without disturbing the Caddyfile structure:

# Example: SaaS Routing with an External File Database
example.com {
    # Map the client request host to the external database upstream
    map {host} {tenant_upstream} {
        # We use the import directive to load the mapping database
        import /etc/caddy/tenants_mapping.txt
        
        # Default fallback address if the domain isn't registered yet
        default "default-signup-landing:8080"
    }

    reverse_proxy {tenant_upstream}
}

The contents of the /etc/caddy/tenants_mapping.txt file are simple space-separated lines:

# Tenant domain mapping database file
client-a.com tenant-a-service:8081
client-b.com tenant-b-service:8082
www.client-c.org tenant-c-service:8083

With the structure above, your DevOps team can instantly update the tenants_mapping.txt file, then trigger the caddy reload command to safely apply the new mappings without downtime.


Security Patterns: Bot Detection & Automatic Blocking #

You can leverage the map directive to detect malicious scraper bots or web crawlers based on the User-Agent header they send, then automatically block access or provide custom responses that save your server bandwidth.

# Example: Dynamic Malicious Bot Mitigation
example.com {
    # 1. Map the User-Agent header to the blocking status
    map {header.User-Agent} {is_malicious_bot} {
        # Detect known resource-draining bot agent strings
        ~(?i)semrushbot         "block"
        ~(?i)ahrefsbot          "block"
        ~(?i)mj12bot            "block"
        ~(?i)dotbot             "block"
        ~(?i)rogue-crawler      "block"
        
        # Human visitors or good search engines (Google/Bing)
        default                 "allow"
    }

    # 2. Block if a malicious bot is detected
    @blocked_request expression {is_malicious_bot} == "block"
    handle @blocked_request {
        # Return an instant 403 Forbidden response at the edge server
        respond "Access Denied: Crawlers are not allowed on this domain." 403
    }

    # 3. Normal routes for real visitors
    handle {
        root * /var/www/html
        file_server
    }
}

Other Advanced Production Use Cases #

Use Case 1: Dynamic Maintenance Mode #

When doing a large database update, you often need to activate maintenance mode instantly. However, you (the developer team) must still be able to access the website to test the update results, while general users must be redirected to the maintenance page.

You can use map to map visitor IP addresses. If the IP is a dev team IP, grant access; if it’s an external IP, throw the maintenance status.

# Example: IP-Based Dynamic Maintenance Mode
example.com {
    # 1. Map the visitor IP ({remote_host}) to the maintenance status
    map {remote_host} {maintenance_status} {
        # Developer Team IPs exempted from maintenance
        "192.168.1.50"  "bypass"
        "203.0.113.10"  "bypass"
        "36.85.12.99"   "bypass"
        
        # General users are redirected to maintenance mode
        default         "maintenance"
    }

    # 2. Define routes based on the mapping result
    # If the status is maintenance, serve the static maintenance page
    @under_maintenance expression {maintenance_status} == "maintenance"
    handle @under_maintenance {
        root * /var/www/maintenance
        rewrite * /index.html
        file_server
    }

    # If the status is bypass, forward the request to the main application backend
    handle {
        reverse_proxy app-backend:8080
    }
}

Use Case 2: Dynamic Security Header Adjustment Based on Routes #

Some pages on your website — like sensitive payment pages — require a very strict Content Security Policy (CSP). However, ordinary public pages (like blogs with external social media plugins) need a more relaxed CSP.

You can map the URL path ({path}) to custom CSP rule bodies dynamically:

# Example: Dynamic CSP Configuration
example.com {
    # Map routes to CSP rules
    map {path} {csp_policy} {
        # Admin routes: completely forbid inline scripts
        ~^/admin/ "default-src 'self'; script-src 'self'"
        
        # Transaction routes: very strict
        ~^/checkout/ "default-src 'self'; connect-src 'self' api.payment-gateway.com"
        
        # Public blog routes: allow external social media plugins
        ~^/blog/ "default-src 'self'; script-src 'self' platform.twitter.com"
        
        # Default global CSP for other routes
        default "default-src 'self'"
    }

    # Set the security response header using the dynamic mapping result variable
    header Content-Security-Policy {csp_policy}

    root * /var/www/html
    file_server
}

Use Case 3: A/B Testing Backend Routing (Canary Deployments) #

When releasing a new version of your backend application (v2.0), you may want to test its stability first by routing 10% of user traffic to the new server (Canary Server), while 90% of users stay on the stable server (v1.0).

You can leverage Caddy’s random number generation function combined with the map directive:

# Example: Canary Deployment A/B Testing
example.com {
    # We use Caddy's built-in UUID placeholder to generate random characters.
    # We take the last character of the request UUID, then map it
    # to split traffic evenly based on that last character's hash value.
    map {http.request.uuid} {upstream_server} {
        # If the UUID's last character ends with 0, 1, or 2 (~30% traffic)
        ~[0-2]$ "canary-backend:8080"
        
        # Other characters (~70% traffic) go to the stable server
        default "stable-backend:8080"
    }

    # Add a custom header for log analysis on the backend
    header_up X-Routing-Group {upstream_server}

    reverse_proxy {upstream_server}
}

Lookup Table Performance Optimization in Caddy #

The map directive is executed by Caddy for every incoming HTTP request. Therefore, you must ensure your lookup table is designed efficiently so it doesn’t add latency to the web server’s internal processing.

Here are some performance optimization guidelines you must pay attention to:

  1. Prioritize Static Matching: Caddy searches lookup table row matches from top to bottom sequentially. Exact static rules evaluate much faster than regular expression (regex) rules. Put static matches at the top of the lookup table, and regex matches at the bottom.
  2. Avoid Complex Regex: If you use regex in the lookup table, make sure your regex is very efficient and doesn’t trigger excessive backtracking computation that can increase server CPU usage.
  3. Use Default Values Wisely: Always define the default row on every map block. If the client input doesn’t match and no default row is defined, Caddy fills an empty string ("") into the output variables. This can trigger system failures on subsequent directives (e.g., reverse_proxy trying to call an empty address).

Summary #

  • Dynamic Logic Center: The map directive maps Caddy input/placeholder variable values to new custom output variables using an efficient lookup table.
  • Data Centralization: Helps you separate routing logic from mapping data, keeping the Caddyfile lean and modular.
  • Multiple Output Variables: Can produce several output variables in parallel from a single input evaluation (like determining language and text writing direction).
  • Matching Flexibility: Supports exact static string matching, regular expression (regex) matching using the tilde prefix (~), and subdomain wildcard matching.
  • External SaaS Database: Supports loading mass mapping tables from private external files to simplify managing routing for thousands of domain customers.
  • Spammer Bot Mitigation: Identifies malicious bot agent identities in the lookup table to trigger instant HTTP 403 blocking at the edge server level.
  • Canary Deployment: Makes setting up dynamic backend route A/B testing easy by leveraging random UUID placeholders.
  • Mandatory Default Values: Always include the default fallback rule in the lookup table to avoid empty-valued variables that can break the reverse proxy flow.

← Previous: Templates   Next: Logging →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact