Virtual Host #

Virtual Hosting is a technology that lets one physical server or one web server instance serve many different domain names and sites simultaneously. In the modern infrastructure world, this is a very common need: from individual developers who want to run several portfolio projects on one cheap VPS, to enterprise-level organizations that need to manage hundreds of application subdomains from one centralized server cluster. With virtual hosting, server resource utilization can be maximized without paying extra costs for new hardware.

Caddy supports virtual hosting configuration natively with very clean, easy-to-understand syntax. In the Caddyfile, every site block is essentially defined as one independent virtual host. Caddy dynamically isolates the configuration, static file base directories, routing logic, and even the TLS certificate lifecycle for each of those hosts without any risk of overlap.


Name-Based Virtual Hosting Mechanics & SNI #

The most commonly used form of virtual hosting today is Name-Based Virtual Hosting. In this method, the server distinguishes which site a visitor is targeting based on the HTTP Host header sent by the browser.

However, in the modern web era where nearly all traffic uses HTTPS encryption, a new challenge appears: the server must present the correct TLS certificate before the browser sends the HTTP Host header. To solve this, the TLS protocol uses an extension called Server Name Indication (SNI).

HTTPS Virtual Host Connection Processing Flow via SNI:

1. The Browser Starts the Connection:
   The browser sends a TLS Client Hello message to the server's IP. Inside this message,
   the SNI extension includes the target hostname (e.g., "api.example.com").
   
2. Caddy Reads the SNI:
   Caddy captures the SNI extension before the TLS handshake completes.
   
3. Isolated Certificate Selection:
   Caddy looks up the matching TLS certificate for "api.example.com" in its local storage
   and presents it to the browser.
   
4. TLS Handshake Completes & Request Sent:
   The encrypted channel is established. The browser now sends the HTTP request containing
   the "Host: api.example.com" header.
   
5. Caddyfile Routing:
   Caddy matches the Host header against site blocks in the Caddyfile and forwards
   the request to the appropriate handler (e.g., a backend reverse proxy).

This cycle runs automatically in Caddy. Every virtual host you declare in the Caddyfile gets registered with Caddy’s TLS engine for dynamic SNI certificate management.

In memory, Caddy maintains a highly efficient certificate cache. When a TLS handshake happens, Caddy looks up this map using the SNI hostname as the key. If a matching, still-valid certificate is found, Caddy uses it immediately. If the certificate is approaching expiry, Caddy’s background engine is triggered to renew it without disturbing active connections.


Basic Multi-Domain Configuration #

Here’s an example of a standard Caddyfile configuration hosting several different domains and subdomains on one server:

# Global options
{
    email [email protected]
}

# Virtual Host 1: Main Marketing Site (Static Files)
example.com {
    root * /var/www/landing-page/dist
    encode gzip zstd
    file_server
}

# Virtual Host 2: API Service Subdomain (Reverse Proxy)
api.example.com {
    reverse_proxy localhost:8080
    
    # Add special CORS headers for the API
    header {
        Access-Control-Allow-Origin "https://example.com"
        Access-Control-Allow-Methods "GET, POST, OPTIONS"
    }
}

# Virtual Host 3: Corporate Blog (Different Domain)
other-blog.com {
    root * /var/www/blog
    encode gzip
    file_server
}

# Virtual Host 4: Redirecting an Old Domain to a New One
old-brand.com {
    # Redirect the old domain permanently (HTTP 301) preserving the URI
    redir https://example.com{uri} permanent
}

Each site block above gets a separate TLS certificate. Caddy monitors each domain’s expiry date independently and renews them in rotation in the background without interfering with each other.


Redirect Patterns: WWW vs Non-WWW (Canonical) #

To maintain SEO quality (Search Engine Optimization) and avoid duplicate content penalties from search engines like Google, you must redirect all traffic to one canonical domain version (whether using the www prefix or directly to the non-www parent domain).

Here are the 4 canonical redirect patterns supported by Caddy:

Pattern 1: Separate Redirect Block (www to non-www) #

This is the cleanest and recommended method because it separates the main logic from the redirect logic:

# Secure www and redirect it
www.example.com {
    redir https://example.com{uri} permanent
}

# Main application block
example.com {
    root * /var/www/html
    file_server
}

Pattern 2: Separate Redirect Block (non-www to www) #

The opposite of Pattern 1, used when your canonical domain wants to keep the www prefix:

example.com {
    redir https://www.example.com{uri} permanent
}

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

Pattern 3: Shared Handling Without Canonical Redirect #

Both domains serve the same content without forcing a redirect (not recommended for SEO because it’s considered duplicate content):

example.com, www.example.com {
    root * /var/www/html
    file_server
}

Pattern 4: Internal Redirect in One Block #

Combines both domains in one block but does an internal redirect using a hostname matcher:

example.com, www.example.com {
    # Host matcher to detect the www domain
    @www host www.example.com
    redir @www https://example.com{uri} permanent
    
    root * /var/www/html
    file_server
}

Dynamic Subdomain Routing in One Block #

If you manage a multi-tenant application where each new user gets a dedicated subdomain (e.g., user1.example.com, user2.example.com), writing a separate site block for each subdomain in the Caddyfile is very impractical.

The best solution is using a Wildcard Subdomain and handling routing with host matchers inside a single site block:

# Dynamic Subdomain Caddyfile
example.com, *.example.com {
    # DNS challenge is required for wildcard SSL
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    
    # 1. Define Host Matchers
    @apex host example.com
    @api  host api.example.com
    @docs host docs.example.com
    
    # 2. Handling Logic for Each Host
    handle @apex {
        root * /var/www/landing
        file_server
    }
    
    handle @api {
        reverse_proxy localhost:8080
    }
    
    handle @docs {
        root * /var/www/documentation
        file_server
    }
    
    # 3. Handling for Dynamic User Subdomains
    # If it doesn't match apex, api, or docs, it's assumed to be a tenant subdomain
    handle {
        # We can use headers to forward to our SaaS backend
        header X-Tenant-Subdomain {labels.2} # Grab the dynamic subdomain label
        reverse_proxy tenant-service:3000
    }
}

In Caddy, the {labels.2} placeholder refers to the domain label counted from the back (0-indexed). For user1.example.com:

  • {labels.0} = com
  • {labels.1} = example
  • {labels.2} = user1 (the dynamic subdomain)

Port-Based Virtual Hosting (Non-Standard Ports) #

Although Caddy listens for web traffic on the standard HTTP port 80 and HTTPS port 443 by default, there are situations where you need to run Caddy on custom ports. This is common in internal office environments, behind corporate firewalls, or when Caddy runs as a Docker container whose ports are specially mapped by an orchestration system.

Caddy fully supports defining custom ports directly in the hostname header of the Caddyfile:

# Listen on a non-standard port
http://internal.example.local:8080 {
    root * /var/www/internal-docs
    file_server
}

https://secure.example.local:8443 {
    # Because this is a non-standard port, we must specify internal TLS configuration
    # so Caddy doesn't fail to complete the external Let's Encrypt HTTP challenge
    tls internal
    
    reverse_proxy localhost:9000
}

[!NOTE] If you explicitly include the http:// protocol scheme before the domain name, Caddy automatically disables the Automatic HTTPS feature for that block and only serves insecure HTTP traffic on the specified port. Conversely, including https:// enables Caddy’s TLS engine to listen for TLS handshakes on that custom port.


Snippets for DRY Configuration (Don’t Repeat Yourself) #

When you manage dozens of virtual hosts in the Caddyfile, you often write the same configuration repeatedly (like security headers, compression settings, and logging). Caddy provides the Snippet feature to wrap those recurring configurations for efficient reuse.

Key Example: Snippets for Critical Reverse Proxies #

In production environments, you often need special compression handling and robust proxy parameter settings to prevent gateway failures:

# 1. Define a Security Header Snippet
(security_headers) {
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
        # Remove the web server identity from the public
        -Server
    }
}

# 2. Define a Logging Snippet with Dynamic Parameters
(access_log) {
    log {
        # Use the dynamic {args[0]} argument to differentiate log file names
        output file /var/log/caddy/{args[0]}-access.log {
            roll_size 50mb
            roll_keep 10
        }
        format json
    }
}

# 3. Snippet for Reverse Proxy Optimization
(proxy_settings) {
    reverse_proxy {
        # Set connection timeouts so the proxy doesn't hang
        dial_timeout 5s
        read_timeout 30s
        
        # Load balancing policy if you have backup upstreams
        lb_policy round_robin
    }
}

# 4. Define a Composite Snippet (Importing Other Snippets)
(standard_site) {
    encode gzip zstd
    import security_headers
}

# ── Applying Snippets to Virtual Hosts ────────────────────────

example.com {
    root * /var/www/main
    import standard_site
    import access_log "main-site"
    file_server
}

blog.example.com {
    root * /var/www/blog
    import standard_site
    import access_log "blog"
    file_server
}

Path-Based Virtual Hosting #

Besides distinguishing routing by hostname (name-based), Caddy also supports URL path-based routing inside the same virtual host. This is useful when you want to serve a static frontend and a backend API under a single domain without separate subdomains:

# URL path-based routing
example.com {
    # 1. The /app/* path routes to the SPA Frontend
    handle /app/* {
        # Strip the "/app" prefix so index.html is looked up at the root, not root/app/
        uri strip_prefix /app
        
        root * /var/www/app/dist
        try_files {path} /index.html
        file_server
    }
    
    # 2. The /api/* path routes to the Node.js Backend
    handle /api/* {
        reverse_proxy localhost:8080
    }
    
    # 3. Default fallback path (Static Landing Page)
    handle {
        root * /var/www/landing
        file_server
    }
}

Environment-Based Configuration Management #

In modern development workflows, you need different virtual host configurations for local environments (your work computer) and the real production server. The Caddyfile supports environment variables to ease this transition without changing configuration code.

Production Caddyfile: #

# Configuration on the Production Server
{
    email [email protected]
}

# Use an environment variable for the domain
{env.DOMAIN_NAME} {
    import security_headers
    root * /var/www/html
    file_server
}

Local Development Caddyfile: #

# Configuration on the Developer Computer (Caddyfile.dev)
{
    # Use the Local Certificate Authority
    local_certs
}

# Local domain for development
app.localhost {
    tls internal
    reverse_proxy localhost:3000
}

Virtual Host Diagnosis and Monitoring #

Caddy provides a local administrative API on port 2019 to inspect the virtual host mapping currently active in memory:

# 1. Get the list of all virtual hosts registered on Caddy's HTTP server
curl -s http://localhost:2019/config/apps/http/servers/ | jq 'keys'
# Success output:
# [ "srv0" ]

# 2. Inspect the routing details on the srv0 server
curl -s http://localhost:2019/config/apps/http/servers/srv0/routes | jq .

# 3. Test the Host header response locally using curl
curl -H "Host: api.example.com" http://localhost:80

Summary #

  • Site Block as Host — Every site block in the Caddyfile is defined as one independent virtual host with its own TLS certificate.
  • SNI Technology — Caddy reads the Server Name Indication (SNI) extension during the TLS handshake to select the right certificate before the HTTP Host header is received.
  • Canonical Redirects — Use a separate site block with the redir directive to redirect www subdomains to the main domain to maintain SEO health.
  • Port-Based Virtual Hosting — Caddy supports listening on custom non-standard ports with custom protocol prefixes (http:// or https://).
  • Wildcard Subdomains — Use the host matcher inside one wildcard block (*.domain.com) to route dynamic subdomains to different backend services.
  • DRY Snippets — Leverage Snippets to consolidate security header, compression, and logging configuration so there’s no code duplication in the Caddyfile.
  • Path-Based Routinghandle routing locations combined with path matchers (e.g., /api/*) let you serve APIs and static files under the same domain name.

← Previous: Static Files   Next: File Server →

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