You can serve an entire constellation of distinct websites from a single Caddy instance, each with its own domain, TLS certificates, and content, by defining virtual hosts.

Let’s say you have two sites: site1.example.com and site2.example.com. Your Caddyfile would look like this:

site1.example.com {
    root * /var/www/site1
    file_server
}

site2.example.com {
    root * /var/www/site2
    file_server
}

When a request comes in for site1.example.com, Caddy matches it to the first block and serves files from /var/www/site1. For site2.example.com, it uses the second block and serves from /var/www/site2. Caddy automatically handles obtaining and renewing TLS certificates for both site1.example.com and site2.example.com using Let’s Encrypt.

This setup is incredibly powerful for consolidating multiple web presences onto a single server. Instead of managing separate web server instances for each domain, you manage one Caddy process. This simplifies configuration, resource utilization, and security patching. Each site block in the Caddyfile is treated as an independent virtual host, allowing for granular control over its specific directives like root, reverse_proxy, log, and more.

The core of this functionality lies in Caddy’s SNI (Server Name Indication) awareness. When a TLS connection is established, the client’s browser sends the requested hostname as part of the handshake. Caddy inspects this hostname and selects the appropriate Caddyfile block to handle the request. If the hostname doesn’t match any defined blocks, Caddy will typically respond with a default page or an error, depending on its configuration.

Consider a scenario where you’re hosting a static marketing site and a dynamic API.

marketing.example.com {
    root * /var/www/marketing
    file_server
}

api.example.com {
    reverse_proxy localhost:8080
}

Here, marketing.example.com serves static files, while api.example.com forwards requests to a backend application running on localhost:8080. Caddy manages TLS for both domains transparently.

You can also group related virtual hosts using the * wildcard or by defining a base configuration that other hosts inherit from. For example, to apply a common set of logging settings to multiple sites:

{
    log {
        output stdout
        format json
    }
}

site1.example.com {
    root * /var/www/site1
    file_server
}

site2.example.com {
    root * /var/www/site2
    file_server
}

In this case, both site1.example.com and site2.example.com will inherit the JSON logging configuration defined in the global block.

The real magic, however, is how Caddy handles domain matching. It’s not just simple string comparison. Caddy can match based on IP address, specific subdomains, or even patterns. For instance, you could have a catch-all for any subdomain of example.com that doesn’t have a specific configuration:

*.example.com {
    root * /var/www/default-subdomain
    file_server
}

specific.example.com {
    root * /var/www/specific
    file_server
}

If a request comes in for another.example.com, it will match the *.example.com block. If it’s for specific.example.com, that block takes precedence. This allows for flexible and hierarchical configuration.

When you restart Caddy after modifying your Caddyfile, it reloads the configuration. You can test your changes without downtime by using caddy reload. Caddy will gracefully switch to the new configuration, ensuring no requests are dropped during the update.

The most surprising thing about Caddy’s virtual host handling is its implicit TLS management. You don’t need to explicitly configure ACME challenges or certificate paths for each site if you’re using publicly resolvable domain names. Caddy just does it. It automatically detects that a request is for a new domain, initiates the ACME challenge process (usually HTTP-01 or TLS-ALPN-01), obtains the certificate, and serves it. This automatic certificate management is a cornerstone of its ease of use for multi-site hosting.

The underlying mechanism for matching requests to Caddyfile blocks involves Caddy parsing the incoming request’s host header and comparing it against the defined site addresses. For TLS-enabled sites, it also leverages the SNI extension in the TLS handshake. If multiple site blocks could potentially match a request, Caddy applies a specific precedence order. More specific matches (like specific.example.com) will always take precedence over less specific matches (like *.example.com or a bare domain like example.com). This ensures that your intended configuration is always applied.

The next logical step is to explore how to use templates and dynamic configuration to manage a very large number of virtual hosts without a monolithic Caddyfile.

Want structured learning?

Take the full Caddy course →