| |

LFCE 54 ๐Ÿง Web Server โ€” Caddy Basics

โฑ๏ธ Estimated read: 12 min | |

Caddy is a modern web server and reverse proxy whose defining feature is automatic HTTPS. Where Nginx and Apache require the administrator to obtain certificates, configure the TLS directives, and set up a renewal timer, Caddy does all of this without configuration. A site block with a public hostname is enough: Caddy obtains a certificate from Let’s Encrypt or ZeroSSL, serves the site over HTTPS, redirects HTTP to HTTPS, and renews the certificate before it expires . The previous chapters covered Nginx and Apache, both of which require explicit TLS configuration; this chapter covers the server that treats TLS as the default rather than an add-on.

The configuration language is the Caddyfile, a compact format built from site blocks and directives. It is simpler than the Nginx server/location model and the Apache <VirtualHost>/<Directory> containers, but it is not less capable: the reverse_proxy, file_server, and root directives cover the common deployment patterns, and the handle and route blocks provide path-based routing . The service is managed through systemctl, and the configuration is validated with caddy validate before every reload.

Key point: A site address with a public hostname triggers automatic HTTPS. Caddy obtains the certificate, configures TLS, and redirects HTTP to HTTPS without any additional directives. For local development or internal hosts, the automatic HTTPS uses Caddy’s internal CA, or the http:// scheme disables it explicitly . Always run caddy validate --config /etc/caddy/Caddyfile before reloading; a syntax error prevents the new configuration from being applied.


Why Caddy replaces explicit TLS configuration

The certificate-management problem.

Nginx and Apache separate the certificate acquisition from the server configuration. The administrator runs Certbot, obtains the certificate, writes the ssl_certificate directives, and relies on a systemd timer for renewal. Caddy integrates all of these steps. When a site block contains a hostname like example.com, Caddy registers the domain with an ACME certificate authority, completes the HTTP-01 or TLS-ALPN challenge, installs the certificate, and schedules the renewal . The administrator does not write a single TLS directive.

The configuration-simplicity problem.

The Caddyfile is designed to be read and written by humans. A static file server is two lines: the site address and the root and file_server directives. A reverse proxy is two lines: the site address and the reverse_proxy directive. There are no separate listen, server_name, and location blocks to coordinate. The directive order is handled by Caddy’s built-in ordering, and the handle and route blocks provide explicit control when the default order is not what the application needs .

The HTTP-to-HTTPS-redirect problem.

Nginx and Apache require an explicit redirect block on port 80. Caddy performs the redirect automatically. When a site is configured with a hostname, Caddy listens on both port 80 and port 443, serves the site over HTTPS, and redirects HTTP requests to the HTTPS URL. The redirect is part of the automatic HTTPS behavior, not a separate configuration . The administrator does not need to write a return 301 directive or a separate server block.


a. The site block and static file serving

A Caddyfile consists of one or more site blocks. Each block begins with a site address and contains directives in curly braces. The simplest static file server uses the root directive to set the document root and the file_server directive to enable static file serving .

# /etc/caddy/Caddyfile
example.com {
    root * /var/www/example
    file_server
}

The root * /var/www/example directive sets the document root for all requests. The * is a matcher that matches every request; it can be omitted when the next argument is a path. The file_server directive enables the static file server, which serves the files from the root path . The site address example.com triggers automatic HTTPS: Caddy obtains a certificate for the domain and serves the site over HTTPS, redirecting HTTP requests to the HTTPS URL.

For local development, the site address can be localhost or a raw IP. Caddy uses its internal certificate authority for these addresses, and the root certificate is installed into the system trust store on first use . The http:// scheme disables automatic HTTPS entirely, which is useful for testing behind a load balancer that already terminates TLS.

# Local development with internal CA
localhost {
    root * /var/www/dev
    file_server
}

# Explicit HTTP, no TLS
http://internal.local {
    reverse_proxy 127.0.0.1:3000
}

The tls internal directive forces the internal CA for a site that would otherwise use a public certificate. The tls directive can also be used to specify custom certificate files or to configure DNS challenge for wildcard certificates .


b. The reverse proxy directive

The reverse_proxy directive forwards requests to a backend server. It is the directive that turns Caddy into an application gateway, and it is the most common use case after static file serving.

# Proxy all requests to a backend
api.example.com {
    reverse_proxy localhost:5000
}

# Proxy only /api/* and serve static files for everything else
example.com {
    root * /var/www
    reverse_proxy /api/* localhost:5000
    file_server
}

The first block proxies every request to the backend on port 5000. The second block uses a path matcher to proxy only requests that begin with /api/, and serves the rest from the static file root . The order of the directives matters: reverse_proxy is evaluated before file_server in Caddy’s default directive order, so the /api/* requests are handled by the proxy and the remaining requests fall through to the file server .

The handle_path directive strips a path prefix before proxying, which is useful when the backend does not expect the prefix. The handle directive preserves the prefix, which is correct when the application is configured to serve from a subpath .

example.com {
    handle_path /app1/* {
        reverse_proxy localhost:5001
    }
    handle_path /app2/* {
        reverse_proxy localhost:5002
    }
    handle {
        respond "Not found" 404
    }
}

The handle_path blocks strip /app1/ and /app2/ before forwarding to the respective backends. The final handle block catches any request that does not match the earlier prefixes and returns a 404 response .


c. Validation, reload, and the admin API

The caddy validate command parses the Caddyfile and reports syntax errors without affecting the running server. The caddy reload command applies the configuration without dropping connections. The systemctl reload caddy command is the systemd equivalent .

# Validate the configuration
sudo caddy validate --config /etc/caddy/Caddyfile
# Output: Valid configuration

# Reload without dropping connections
sudo systemctl reload caddy
# Or: sudo caddy reload --config /etc/caddy/Caddyfile

The caddy fmt --overwrite /etc/caddy/Caddyfile command formats the file consistently, which catches indentation and brace errors early . The caddy adapt command shows the JSON configuration that the Caddyfile produces, which is useful for debugging routing mistakes .

Caddy also exposes an admin API on port 2019. The API can be used to load a JSON configuration at runtime, but mixing the API and the Caddyfile is a common source of confusion. A reload from the Caddyfile overwrites whatever the API applied, and the change appears to disappear without an error . The recommendation is to choose one source of truth per environment: the Caddyfile for file-based configuration, or the API for dynamic configuration, but not both.


Complete Example Session

# ============================================
# PART 1: INSTALL CADDY
# ============================================
# Debian / Ubuntu: add the official repository, then install
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

# ============================================
# PART 2: CREATE THE SITE DIRECTORY
# ============================================
sudo mkdir -p /var/www/example
echo "<h1>Hello from Caddy</h1>" | sudo tee /var/www/example/index.html

# ============================================
# PART 3: WRITE THE CADDYFILE
# ============================================
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
example.com {
    root * /var/www/example
    file_server
}
EOF

# ============================================
# PART 4: VALIDATE THE CONFIGURATION
# ============================================
sudo caddy validate --config /etc/caddy/Caddyfile
# Output: Valid configuration

# ============================================
# PART 5: RELOAD CADDY
# ============================================
sudo systemctl reload caddy

# ============================================
# PART 6: CHECK THE SERVICE STATUS
# ============================================
sudo systemctl status caddy
# Output: active (running)

# ============================================
# PART 7: TEST THE SITE
# ============================================
curl -I https://example.com
# Output: HTTP/2 200
# The certificate is automatically provisioned by Let's Encrypt

# ============================================
# PART 8: ADD A REVERSE PROXY
# ============================================
sudo tee -a /etc/caddy/Caddyfile > /dev/null <<'EOF'

api.example.com {
    reverse_proxy localhost:5000
}
EOF

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# ============================================
# PART 9: CHECK THE LOGS
# ============================================
sudo journalctl -u caddy -n 20
# Output shows certificate provisioning and request handling

# ============================================
# PART 10: FORMAT THE CADDYFILE
# ============================================
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
# The file is rewritten with consistent indentation

The ten parts demonstrate the complete Caddy setup: installation, content creation, Caddyfile configuration, validation, reload, service verification, HTTPS testing, reverse proxy addition, log inspection, and formatting. The critical step is Part 4: caddy validate catches syntax errors before the reload, and the running configuration remains in effect if the validation fails.


Quick Reference

Caddyfile Directives

DirectivePurposeExample
rootSet the document rootroot * /var/www
file_serverServe static filesfile_server
reverse_proxyForward requests to a backendreverse_proxy localhost:5000
redirHTTP redirectredir https://example.com{uri}
handlePath-based routing (preserves prefix)handle /api/* { ... }
handle_pathPath-based routing (strips prefix)handle_path /app/* { ... }
tlsOverride automatic TLStls internal
encodeCompressionencode gzip

Service Commands

CommandPurpose
caddy validate --config /etc/caddy/CaddyfileValidate syntax
caddy fmt --overwrite /etc/caddy/CaddyfileFormat the file
caddy reload --config /etc/caddy/CaddyfileReload without systemd
systemctl reload caddysystemd reload
systemctl status caddyCheck service state
journalctl -u caddy -fFollow logs

Automatic HTTPS Behavior

Site AddressTLS Behavior
example.comAutomatic public certificate
localhostInternal CA certificate
http://example.comNo TLS
*.example.comWildcard certificate (requires DNS challenge)

Best Practices

โœ… Do This:

# Use a public hostname for automatic HTTPS
example.com {
    root * /var/www
    file_server
}                                                            # โœ…

# Use handle_path to strip prefixes for backends
handle_path /api/* {
    reverse_proxy localhost:5000
}                                                            # โœ…

# Use http:// for local development behind a TLS terminator
http://internal.local {
    reverse_proxy 127.0.0.1:3000
}                                                            # โœ…

# Validate before reloading
caddy validate --config /etc/caddy/Caddyfile                 # โœ…

# Format the file for consistency
caddy fmt --overwrite /etc/caddy/Caddyfile                   # โœ…

โŒ Don’t Do This:

# Forget to validate
systemctl reload caddy    # Syntax error prevents reload       # โŒ

# Mix the admin API and the Caddyfile
# The Caddyfile reload overwrites the API config               # โŒ

# Use a public hostname without DNS pointing to the server
# The ACME challenge fails and the certificate is not issued   # โŒ

# Expect a wildcard certificate without DNS challenge
# Let's Encrypt requires DNS-01 for wildcards                  # โŒ

# Use handle instead of handle_path when the backend expects no prefix
# The backend receives the full path and may 404               # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Certificate not issuedDNS does not point to the server, or ports 80/443 blockedVerify DNS and firewall rules
502 Bad GatewayBackend not running or wrong addresscurl the backend directly
404 on valid pathsWrong site block or root pathCheck the site address and root
Redirect loopBackend also redirects to HTTPSCheck the backend configuration
Configuration not appliedValidation failed or API/Caddyfile conflictcaddy validate; choose one config source
Wildcard certificate failsDNS challenge not configuredAdd the DNS plugin and tls { dns ... }

Real-World Examples

1. Static file server

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

2. Reverse proxy

api.example.com {
    reverse_proxy localhost:5000
}

3. Path-based routing

example.com {
    handle_path /api/* {
        reverse_proxy localhost:5000
    }
    handle {
        root * /var/www
        file_server
    }
}

4. Local development

localhost {
    root * /var/www/dev
    file_server
}

5. HTTP-only behind a load balancer

http://internal.local {
    reverse_proxy 127.0.0.1:3000
}

6. Redirect www to apex

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

7. Validate configuration

caddy validate --config /etc/caddy/Caddyfile

8. Format the Caddyfile

caddy fmt --overwrite /etc/caddy/Caddyfile

9. Reload the service

systemctl reload caddy

10. Check logs

journalctl -u caddy -n 50

Visual

Automatic HTTPS Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  A HOSTNAME IN THE SITE BLOCK TRIGGERS AUTOMATIC HTTPS       โ”‚
โ”‚                                                              โ”‚
โ”‚  example.com {                                               โ”‚
โ”‚      root * /var/www                                         โ”‚
โ”‚      file_server                                             โ”‚
โ”‚  }                                                           โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Caddy detects a public hostname                             โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ Obtains certificate from Let's Encrypt/ZeroSSL     โ”‚
โ”‚       โ”œโ”€โ”€ Serves the site over HTTPS                         โ”‚
โ”‚       โ”œโ”€โ”€ Redirects HTTP to HTTPS                            โ”‚
โ”‚       โ””โ”€โ”€ Renews before expiry                               โ”‚
โ”‚                                                              โ”‚
โ”‚  No tls directives required.                                 โ”‚
โ”‚  No Certbot timer.                                           โ”‚
โ”‚  No manual renewal.                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Caddyfile Structure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SITE BLOCKS AND DIRECTIVES                                  โ”‚
โ”‚                                                              โ”‚
โ”‚  example.com {              โ† Site address                   โ”‚
โ”‚      root * /var/www        โ† Directive with matcher         โ”‚
โ”‚      file_server            โ† Directive                      โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  api.example.com {          โ† Second site block              โ”‚
โ”‚      reverse_proxy localhost:5000                            โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  The address determines the hostname and the TLS behavior.   โ”‚
โ”‚  The directives inside the block handle the requests.        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

handle vs handle_path

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PREFIX PRESERVATION                                          โ”‚
โ”‚                                                              โ”‚
โ”‚  handle_path /api/* {                                        โ”‚
โ”‚      reverse_proxy localhost:5000                            โ”‚
โ”‚  }                                                           โ”‚
โ”‚  Request: /api/users โ†’ backend receives /users               โ”‚
โ”‚  (prefix stripped)                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  handle /api/* {                                             โ”‚
โ”‚      reverse_proxy localhost:5000                            โ”‚
โ”‚  }                                                           โ”‚
โ”‚  Request: /api/users โ†’ backend receives /api/users           โ”‚
โ”‚  (prefix preserved)                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Configuration file/etc/caddy/Caddyfile
Automatic HTTPSTriggered by a public hostname
Certificate authorityLet’s Encrypt, ZeroSSL
Local developmentInternal CA for localhost or raw IP
Static file servingroot + file_server
Reverse proxyreverse_proxy
Path routinghandle (preserves), handle_path (strips)
Validationcaddy validate --config /etc/caddy/Caddyfile
Reloadsystemctl reload caddy
Formattingcaddy fmt --overwrite
Admin API port2019

Key takeaways:

  • Caddy’s automatic HTTPS eliminates the TLS configuration step. A site block with a public hostname obtains a certificate, serves HTTPS, redirects HTTP, and renews automatically. No ssl_certificate, no Certbot timer, no renewal configuration .
  • The Caddyfile is a compact, directive-based format. A site address followed by a block of directives. root and file_server for static files, reverse_proxy for backends, handle and handle_path for path routing .
  • handle_path strips the prefix; handle preserves it. The choice depends on whether the backend expects to receive the path prefix. Most applications that are not designed for a subpath require handle_path .
  • Validate before reloading. caddy validate catches syntax errors before the configuration is applied. The caddy fmt command catches indentation and brace errors early .
  • The admin API and the Caddyfile are separate configuration sources. Mixing them causes changes to disappear silently. Choose one source of truth per environment .
  • Wildcard certificates require DNS challenge. Let’s Encrypt requires the DNS-01 challenge for wildcard domains. The DNS plugin for the provider must be installed, and the tls { dns ... } directive configures it .

Remember: Caddy is the web server that treats HTTPS as the default. A site block with a hostname is enough to obtain a certificate, serve the site, and redirect HTTP. The Caddyfile is simpler than the Nginx and Apache equivalents, but it covers the same deployment patterns: static files, reverse proxy, and path routing. Validate with caddy validate, format with caddy fmt, reload with systemctl reload caddy, and test with curl. The certificate is managed automatically; the administrator’s job is to point the DNS, open the firewall, and write the directives that route the requests.


Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!