Snippet & Import #

When you first use Caddy, a small Caddyfile with a few lines of configuration is enough to serve your website. However, as your infrastructure grows — more subdomains, strict security header policies applied across all domains, traffic log separation, and authentication handling — your Caddyfile gradually gets longer, more complex, and filled with duplicated code.

In software engineering, code duplication is the main enemy of system maintainability. If you copy 20 lines of security header configuration into 10 different site blocks, and later need to update one header parameter, you’re forced to make changes in 10 different places manually. The risk of oversight is very high.

Caddy solves this challenge by providing two very powerful modularity features: Snippet and Import. Together, they apply the DRY (Don’t Repeat Yourself) principle to the Caddyfile, letting you build clean, structured, maintainable, and enterprise-ready configurations.


Caddy’s Multi-File Compilation Workflow #

When Caddy reads the main Caddyfile, it processes import instructions recursively and assembles all snippets and external files into a single configuration structure in memory before adapting it to native JSON.

To visualize how Caddy compiles this modular configuration, look at the flow diagram below:

flowchart TD
    Main["Main Caddyfile (/etc/caddy/Caddyfile)"] --> LoadGlobal["1. Load Global Options Block"]
    Main --> ImportSnippets["2. Import Modular Snippets\n(import snippets/*.caddyfile)"]
    Main --> ImportSites["3. Import Site Configurations\n(import sites/*.caddyfile)"]
    
    subgraph Filesystem["Modular Filesystem"]
        direction LR
        S1["snippets/security.caddyfile"]
        S2["snippets/logging.caddyfile"]
        S3["sites/app1.caddyfile"]
        S4["sites/app2.caddyfile"]
    end
    
    ImportSnippets -.-> S1
    ImportSnippets -.-> S2
    ImportSites -.-> S3
    ImportSites -.-> S4
    
    S1 --> Compile["Caddy Engine Compiler (caddy adapt)"]
    S2 --> Compile
    S3 --> Compile
    S4 --> Compile
    Compile --> JSON["Main JSON Config (Active Memory)"]

What Is a Snippet? #

Snippet is a named configuration block defined once and reusable inside any site block in the Caddyfile.

Snippet syntax is declared at the top level of the Caddyfile (outside any site block) by wrapping the snippet name in regular parentheses (snippet_name). The content inside a snippet can contain any valid directive or logic that would normally go inside a site block.

Let’s compare the duplication scenario (Anti-Pattern) with using Snippets (Correct):

# ANTI-PATTERN: Copying a set of security headers into every domain
domain1.com {
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        -Server
    }
    file_server
}

domain2.com {
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        -Server
    }
    reverse_proxy localhost:3000
}

# ====================================================================
# CORRECT: Using Snippets to Eliminate Duplication (DRY)
# ====================================================================

# 1. Define the Snippet at the top of the file
(security_headers) {
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        -Server
    }
}

# 2. Import the Snippet into each domain using 'import'
domain1.com {
    import security_headers
    file_server
}

domain2.com {
    import security_headers
    reverse_proxy localhost:3000
}

With snippets, if you want to change the security header values or add a new header, you only edit it once inside the (security_headers) snippet. The change applies immediately to every site block that imports it.


Passing Dynamic Arguments to Snippets #

Often, your configuration is almost identical but has one or two parameter values that differ. For example, you want the same logging configuration structure for all sites, but the log filename must follow each domain name.

The Caddyfile supports passing dynamic arguments into snippets, similar to passing parameters to a function in a programming language. You access argument values using the {args[0]}, {args[1]}, and so on syntax, based on their positional order at import time.

Here’s an implementation example:

# 1. Define the Snippet with Argument Placeholders
(standard_logging) {
    log {
        # {args[0]} will be replaced with the first argument passed
        output file /var/log/caddy/{args[0]}.log {
            roll_size 50mb
            roll_keep 5
        }
        format json
    }
}

(custom_proxy) {
    # {args[0]} = backend address, {args[1]} = connection timeout
    reverse_proxy {args[0]} {
        transport http {
            dial_timeout {args[1]}s
        }
    }
}

# 2. Use the Snippet by Passing Value Parameters
site1.com {
    import standard_logging "site1-access"
    import custom_proxy localhost:3000 5
}

site2.com {
    import standard_logging "site2-access"
    import custom_proxy localhost:4000 10
}

Handling Arguments with Spaces or Quotes #

If you pass an argument containing spaces or special characters into a snippet, you must wrap that argument in double quotes on the import line so the parser doesn’t split it into separate parameters:

(custom_header) {
    header X-Custom-Message "{args[0]}"
}

example.com {
    # Pass a long string argument with spaces
    import custom_header "Hello from the Production Caddy Web Server"
    file_server
}

Importing External Configurations (Multi-File) #

The import instruction isn’t limited to reading internal snippets written in the same file. You can also use it to include content from external Caddyfile files.

This feature supports wildcard (glob patterns) like the asterisk (*), letting you split configuration into dozens of separate files and import them automatically in one instruction line.

# Import site configuration files from a separate directory
import /etc/caddy/sites-enabled/*.caddyfile

# Import security-specific snippets from a certain file
import /etc/caddy/snippets/security.conf

Caddyfile Modular Structure Comparison Table #

Here’s a comparison table between a single monolithic config file, internal snippets, and multi-file modularity:

Analysis CriteriaMonolithic Single FileInternal Snippets (One File)Multi-File Structure (Import)
Complexity ScaleVery Low (1-2 sites)Medium (3-5 sites)High / Enterprise (>5 sites)
Setup EaseVery EasyEasyRequires planned folder structure
Readability LevelPoor as line count growsGood (internal DRY)Very Good (fully modular)
Team Collaboration (Git)Often triggers merge conflictsFewer merge conflictsVery collaboration-friendly (one file per site)
Debugging EaseEasy (one file location)EasyRequires tracking external import files
Recommended ScenarioQuick tests / SandboxSmall personal serverEnterprise production / Cloud

Large-Scale Multi-File Organization Best Practices #

For large-scale production deployments with many domains, applying a modular directory structure is strongly recommended. It keeps configuration files tidy and makes it easier for DevOps teams to manage site routes.

/etc/caddy/
  ├── Caddyfile                    ← Main File (Entry Point)
  ├── snippets/
  │   ├── security.caddyfile       ← Collection of security header snippets
  │   ├── logging.caddyfile        ← Collection of log configuration snippets
  │   └── cors.caddyfile           ← Collection of CORS snippets
  └── sites-enabled/
      ├── app1.com.caddyfile       ← App 1 site configuration
      ├── app2.com.caddyfile       ← App 2 site configuration
      └── blog.net.caddyfile       ← Blog site configuration

Main Caddyfile File Implementation #

The main file acts as an orchestrator that loads global settings and imports all other modules:

# 1. Global Options
{
    email [email protected]
}

# 2. Import All Supporting Snippets
import /etc/caddy/snippets/*.caddyfile

# 3. Import All Active Site Blocks
import /etc/caddy/sites-enabled/*.caddyfile

/etc/caddy/snippets/security.caddyfile File Implementation #

Stores all reusable security templates:

(security_strict) {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        -Server
    }
}

(allow_iframe_sameorigin) {
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        -Server
    }
}

/etc/caddy/snippets/logging.caddyfile File Implementation #

Stores modular log templates:

(standard_log) {
    log {
        output file /var/log/caddy/{args[0]}.log {
            roll_size 100mb
            roll_keep 7
        }
        format json
        level INFO
    }
}

/etc/caddy/snippets/cors.caddyfile File Implementation #

Stores CORS templates for cross-domain communication:

(cors_origin) {
    header {
        Access-Control-Allow-Origin "{args[0]}"
        Access-Control-Allow-Methods "GET, POST, OPTIONS"
        Access-Control-Allow-Headers "Content-Type, Authorization"
    }
}

/etc/caddy/sites-enabled/app1.com.caddyfile File Implementation #

A clean, self-contained site configuration leveraging global snippets:

app1.company.com {
    # Import the strict security snippet from security.caddyfile
    import security_strict
    
    # Import a custom log snippet
    import standard_log "app1-access"
    
    root * /var/www/app1
    file_server
}

/etc/caddy/sites-enabled/app2.com.caddyfile File Implementation #

An example API site combining security, logging, and CORS:

api.company.com {
    import allow_iframe_sameorigin
    import standard_log "api-access"
    import cors_origin "https://app1.company.com"
    
    reverse_proxy localhost:8080
}

Conditional Imports Based on Environment #

You can combine the import instruction with environment variables to make Caddy dynamically load different configuration between local development servers and production servers.

# Main Caddyfile
{
    # Read the email dynamically from env
    email {env.ACME_EMAIL}
}

# Caddy imports the custom TLS configuration according to the environment variable
# For example, if {env.ENV_TYPE} is 'dev', Caddy looks for the file 'tls-dev.caddyfile'
# If it's 'prod', Caddy loads 'tls-prod.caddyfile'
import /etc/caddy/snippets/tls-{env.ENV_TYPE}.caddyfile

example.com {
    import tls_config
    reverse_proxy localhost:3000
}

Contents of /etc/caddy/snippets/tls-dev.caddyfile:

(tls_config) {
    # Use the internal CA for local development
    tls internal
}

Contents of /etc/caddy/snippets/tls-prod.caddyfile:

(tls_config) {
    # Use the standard Let's Encrypt ACME with Cloudflare DNS
    tls {
        dns cloudflare {env.CLOUDFLARE_TOKEN}
    }
}

Auto-Reload Behavior (Auto-Reload & Watch) #

When you run Caddy in the foreground with file watching enabled (--watch), Caddy monitors file changes to trigger automatic reloads in real time without stopping server traffic (zero-downtime hot reload).

# Run Caddy with file watching enabled
caddy run --config /etc/caddy/Caddyfile --watch

[!IMPORTANT] Caddy’s --watch feature is very smart. Caddy doesn’t just monitor the main entry-point file /etc/caddy/Caddyfile; it also detects and monitors every external file loaded through the import directive (including glob patterns). If any site file inside /etc/caddy/sites-enabled/ changes, Caddy detects the modification, performs internal validation, and triggers an instant config reload.


Anti-Patterns to Avoid #

Here are some fatal mistakes related to snippet and import usage that often cause the server to fail starting:

1. Defining Snippets Inside a Site Block #

Snippets must be declared at the top level of the Caddyfile (global scope). Writing a snippet definition inside a site block’s curly braces triggers a parser read error.

# ANTI-PATTERN: Nested definition
example.com {
    (my_snippet) {   # ← ERROR! This syntax is invalid inside a site block
        header X-Test "value"
    }
}

# CORRECT: Declare it outside the site block first
(my_snippet) {
    header X-Test "value"
}

example.com {
    import my_snippet
}

2. Unlimited Recursive Imports #

Avoid situations where File A imports File B, and File B also imports File A simultaneously. This causes an infinite loop when Caddy tries to parse the configuration, eventually killing the startup process with out-of-memory or a stack overflow crash.

3. Snippet Name Conflicts with Built-in Directives #

Never give a snippet the exact same name as a built-in Caddy directive (like root, encode, reverse_proxy, etc.). This confuses the parser and makes it treat your import as a directive call with wrong arguments.

# ANTI-PATTERN: Snippet name conflicts with the 'encode' directive
(encode) {
    header X-Encoded "true"
}

# CORRECT: Give a unique, descriptive name
(gzip_plus_header) {
    encode gzip
    header X-Encoded "true"
}

Multi-File Configuration Validation and Troubleshooting #

If you use a multi-file architecture with many import instructions, manually tracing syntax errors can become very tedious. Caddy makes this easier by providing very detailed error messages:

# Validate the main Caddyfile
caddy validate --config /etc/caddy/Caddyfile

If an error occurs in one of the imported files, Caddy doesn’t just say an error exists — it also mentions the absolute path of that external file along with the specific line number that triggered the parsing failure.

You can also use the adaptation command to inspect the final configuration file after all imports have been inserted by the Caddy engine:

caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile > /tmp/resolved_config.json

Summary #

  • Snippets are declared with a name inside parentheses (snippet_name) { ... } at the top of the Caddyfile (global scope).
  • Insert positional arguments ({args[0]}, {args[1]}) to make snippets behave dynamically like programming functions.
  • The import instruction can load configuration from internal snippets and external Caddyfile files.
  • Use wildcard glob patterns (import /etc/caddy/sites-enabled/*.caddyfile) to auto-load dozens of site configurations.
  • Separate the sites-enabled/ and snippets/ folders to keep the file structure tidy on production-scale servers.
  • Run caddy validate before reloading the server to ensure there are no syntax errors in external import files.

← Previous: Matcher   Next: Global Options →

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