Skip to content

Multiple hostnames on one IP

One free-tier instance can happily serve dozens of hostnames — a landing page, a demo, some docs, a status page, whatever. Caddy handles the SNI routing for you. All you need is a DNS record per hostname pointing at the same IP, and a Caddyfile block per hostname.

The pattern

Cloudflare (or your DNS)
├── lab.example.com       ─┐
├── demo.example.com      ─┤ all point at the same instance's public IP
├── status.example.com    ─┤ (e.g., web-1 at 203.0.113.10)
└── docs.example.com      ─┘

Instance (web-1, 203.0.113.10)
└── Caddy on :443
    ├── SNI = lab.example.com     → /var/www/lab
    ├── SNI = demo.example.com    → reverse_proxy localhost:8000
    ├── SNI = status.example.com  → respond "OK"
    └── SNI = docs.example.com    → /var/www/docs

The client's TLS handshake includes the hostname it's connecting to (SNI — Server Name Indication). Caddy uses that to pick which Caddyfile block to serve.

Example Caddyfile

{
    email you@example.com
}

# Landing page
lab.example.com {
    root * /var/www/lab
    file_server
    encode gzip zstd
}

# API — reverse proxy to a local app
demo.example.com {
    reverse_proxy localhost:8000
}

# Static status endpoint
status.example.com {
    respond "OK" 200
}

# Documentation site
docs.example.com {
    root * /var/www/docs
    file_server
    encode gzip zstd

    # Nice-to-have: try_files for pushState-style SPAs
    try_files {path} {path}/ /index.html
}

Reload:

sudo systemctl reload caddy

Caddy will obtain a certificate for each hostname independently, in parallel. If any one fails (e.g., DNS not yet propagated), the others still work.

Wildcard hostnames

If you want to serve anything.example.com for many "anything"s from one block, Caddy supports both explicit wildcards and multi-name blocks:

# Explicit wildcard — needs DNS-01 challenge (see below)
*.example.com {
    root * /var/www/generic
    file_server
}

# Multi-name block — one config, several hostnames, individual certs
a.example.com, b.example.com, c.example.com {
    respond "Same content, three hostnames"
}

Wildcard certificates require the DNS-01 ACME challenge, which needs Caddy to write DNS records dynamically. That means using a DNS plugin (e.g., caddy-dns/cloudflare) and an API token — worth it if you have a lot of hostnames, overkill for a handful.

For under ~10 hostnames, just list them explicitly.

When you outgrow one instance

If a single AMD Micro is serving too many hostnames and CPU-starves, move the busiest ones to the ARM Ampere (which has 4 OCPU and 24 GB — way more than needed). Update the DNS A records to point at the ARM instance's IP, wait for propagation, then remove those blocks from web-1's Caddyfile.

Deploying content to /var/www/

Two patterns:

Pattern 1: SSH + rsync from your local machine

# On your local machine
rsync -av --delete ./dist/ web-1:/tmp/lab-deploy/
ssh web-1 "sudo rsync -av --delete /tmp/lab-deploy/ /var/www/lab/ && sudo systemctl reload caddy"

Wrap this in a deploy.sh per site. See Adding a service for a fuller version.

Pattern 2: Git-based, on the instance

# On web-1, once
sudo mkdir -p /var/www/lab
sudo chown $USER:$USER /var/www/lab
git clone https://github.com/you/lab-site.git /var/www/lab

# To update
cd /var/www/lab && git pull
sudo systemctl reload caddy    # only if the git checkout builds fresh content

Simpler; requires the instance to have git-read access to the repo (SSH key or public repo).

Verifying multiple hostnames

Once DNS is live and Caddy is reloaded:

# From your local machine
curl -sI https://lab.example.com
curl -sI https://demo.example.com
curl -sI https://status.example.com

# All three should return 200 OK with a valid TLS chain.
# The certificate chain sent depends on the SNI:
openssl s_client -connect 203.0.113.10:443 -servername lab.example.com </dev/null 2>/dev/null \
    | openssl x509 -noout -subject
# Should show: subject=CN = lab.example.com

Try with a hostname Caddy doesn't know about:

curl -sI https://notreal.example.com --resolve notreal.example.com:443:203.0.113.10
# Caddy will present its "default" cert (or refuse the handshake, depending
# on config) and typically return 421 Misdirected Request.

Next

Head to Adding a service to run something behind Caddy — a Go binary, a Node app, a systemd unit, whatever.