Caddy Operations Manual

14 min read
On this page

Diagnose a request through ingress, matching, path rewriting and upstream handling. A parseable Caddyfile, loaded configuration and correct public response are separate results to verify.

TaskStart here
Proxy 404 / wrong pathRouting and prefixes
Apply configurationValidate, reload, verify
TLS / client certificatesCertificate ownership and mTLS

The upstream may receive a rewritten path

Preparing the visual
Which path reaches the upstream after stripping?

handle /api/* preserves the path; handle_path strips /api. Bare /api does not match /api/* and reaches this example’s fallback.

Complete site example: static files and /api/* routing. Merge the site block into the real Caddyfile and adapt domain, root, and upstream.

example.com {
    handle_path /api/* {
        reverse_proxy 127.0.0.1:3000
    }
    handle {
        root * /srv/www
        file_server
    }
    log
}

Here /api/users reaches the upstream as /users. Use handle /api/* if the application expects the prefix. /api/* does not match bare /api; handle it deliberately. Handle groups are mutually exclusive with a fallback. file_server does not enable directory listing unless browse is specified.

Reach the upstream from Caddy’s environment first

# Request the rewritten upstream path from the Caddy host
curl -v http://127.0.0.1:3000/users

In a container, loopback refers to that container; test the actual address in the same network namespace. Verify status/body before testing the proxy.

# Test the public path and TLS through the selected entry
curl -v --resolve example.com:443:127.0.0.1 https://example.com/api/users

--resolve specifies name/port/IP. Compare the same resource on both paths and also test /api/, bare /api, and static paths.

# Inspect Caddy diagnostics near the request time
journalctl -u caddy -b -n 100 --no-pager

Distinguish runtime from access logs; configured access logs may be files. Use upstream errors behind 502 to identify protocol/network issues.

Compare the same resource after rewriting. --resolve preserves the URL hostname and SNI while selecting an ingress IP; a Host header alone does not change SNI. Do not use -k to hide a TLS failure.

SymptomInspect
502Upstream address/protocol/namespace and dial/TLS errors; test from Caddy’s environment
404Which layer returned it, matcher, prefix stripping, static root and permissions
403File access, auth and upstream logs; status code alone does not locate the owner

For multiple backends, use a health endpoint that represents readiness to receive requests. A successful probe is not proof of every dependency; several ports on one host do not provide host-level redundancy.

Alternative site: proxy the entire site to multiple backends. Replace the earlier same-domain block rather than append a duplicate; retain a matcher if only /api/* should be proxied.

example.com {
    reverse_proxy 127.0.0.1:3000 127.0.0.1:3001 {
        lb_policy round_robin
        health_uri /health
        health_interval 10s
    }
}

Adapt, validate and reload the actual service

Identify binary, config path, and service identity

# Check the version for the intended configuration syntax
caddy version

Check plugins with caddy list-modules; installing a binary does not prove the service uses it.

# Inspect ExecStart, ExecReload, User, and config location
systemctl cat caddy.service

Substitute the actual path and retain the previous file before editing. The configuration examples are separate alternatives, not duplicate same-domain blocks to concatenate.

# Preview formatting changes without writing
caddy fmt --diff /etc/caddy/Caddyfile

Apply --overwrite separately if desired after review. Formatting is not semantic validation.

# Inspect the JSON adapted from the Caddyfile
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile --pretty

Inspect route order, matchers, and rewrites. Adapt does not start listeners and can expose sensitive configuration.

# Load and validate modules without replacing the running service
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

Use the actual service identity/environment where possible to check access. Fix failures before reload.

# Apply through the verified ExecReload action
sudo systemctl reload caddy.service

Requires a reload-capable unit targeting this file. Check status/logs and whether the previous config still serves after rejection, rather than immediately restarting.

# Check service state, then repeat real route requests
systemctl status caddy.service --no-pager

Verify API, bare-prefix, static, and health paths. To roll back, restore the old file, validate/reload, and repeat the same requests.

fmt formats; adapt produces JSON; validate provisions modules but does not test all live dependencies. Check file access as the service identity. Verify ExecReload targets the intended file. For custom units use the configured caddy reload path. Keep the prior config and inspect which version remains active on failure.

Reload supports config switching, not unconditional continuity for every long-lived stream. Test WebSocket/stream close policies. GET /config on the admin API shows runtime JSON but can contain secrets; keep its access scoped. There is no general standard list-certificates CLI command; inspect served TLS and configured storage.

Give certificates one maintenance owner

Automatic HTTPS needs correct DNS, challenge reachability or DNS-plugin permissions, and persistent writable certificate storage. Issuers are configurable, not only Let’s Encrypt. Manual certificate files need an external renewal/deployment chain; see certificates. HTTP/3 also needs UDP ingress and client support.

Independent mTLS site: the client CA file must exist and be readable by the service. Deploy using the validation/reload workflow above.

internal.example.com {
    tls {
        client_auth {
            mode require_and_verify
            trust_pool file /etc/caddy/client-ca.pem
        }
    }
    reverse_proxy 127.0.0.1:3000
}

Use a version supporting trust_pool. client-ca.pem is a trusted client-issuing CA, not a revocation list. mTLS certificate authentication does not replace application authorization. reverse_proxy already handles common forwarding headers; configure trusted_proxies for known ingress rather than trusting arbitrary X-Forwarded-For.

Test mTLS with both negative and positive requests

Run after installing the mTLS site above: test rejection without a client certificate, then test an approved client identity. Use the site’s approved trust and key files.

# Without a client certificate, verify that mTLS is required
curl -v https://internal.example.com/

Expect TLS rejection. A normal application response may mean a wrong entry or missing client_auth. Use --cacert for a private server CA, not -k.

# Verify access using an approved client certificate
curl -v --cert client.crt --key client.key https://internal.example.com/

After TLS succeeds, verify application authorization; a trusted certificate is not universal business permission.

handle_path · reverse_proxy · CLI · TLS

For the actual edge ingress setup, see the WildWindStudio EdgeOne runbook.