skip to content

Caddy

Caddy is a Go web server whose headline feature is automatic HTTPS: point it at a domain and it obtains, installs and renews certificates for you, with a Caddyfile that is a fraction of the size of an equivalent nginx config. Interviewers ask about it to see whether I can reason about the trade — how much operational simplicity is worth, and what I give up in tuning surface, ecosystem and third-party module availability compared with nginx or HAProxy.

on this pageshow

questions

5

A Caddyfile contains only the site block `example.com { reverse_proxy localhost:8080 }`. Describe what Caddy does about HTTPS when it loads that config, and what has to be true of the host for it to work.

level: juniorimportance: must knowfreq 78%

answer

  1. the config never mentions certificates
  2. the site address decides everything
  3. challenge needs port 80 or 443 reachable
  4. certificates persist in the data directory
  5. localhost and IPs use the internal CA

basics

~20 s

Caddy reads the site address as a public domain, so it obtains a certificate for example.com from a public ACME certificate authority, serves the site over HTTPS, redirects plain HTTP to HTTPS, and renews the certificate on its own. No TLS configuration is written anywhere.

solid answer

~50 s

That config never mentions TLS, and that is the point. Because the site address is a public hostname, Caddy's automatic HTTPS kicks in at config load: it asks a public ACME certificate authority (Let's Encrypt by default) for a certificate covering `example.com`, solves the challenge itself, stores the certificate and key in its data directory, serves the site on 443, and puts a permanent redirect on port 80 for the same name. It then renews in the background while the certificate still has roughly a third of its lifetime left. For that to succeed the host needs public DNS for `example.com` resolving to it and inbound reachability on port 80 (HTTP-01) or 443 (TLS-ALPN-01), and the data directory has to survive restarts. Non-public addresses such as `localhost` or a bare IP get a certificate from Caddy's own internal CA instead.

code

bash · 2 lines
bash
# Where automatic HTTPS keeps what it obtained (Linux, running as your user)
ls -R ~/.local/share/caddy/certificates

go deeper

for a junior

Be ready to say that the domain in the site address is what triggers HTTPS, that Caddy fetches and renews the certificate itself, and that DNS must already point at the machine.

for a middle

Explain the mechanics: which challenge is solved on which port, that the certificate lands in Caddy's data directory, and that renewal happens in the background well before expiry.

for a senior

Show that you treat the data directory as state. Talk about what re-issuance on every restart costs you against a CA's rate limits, and how you would confirm from logs whether a failure is DNS, reachability or the CA.

for a principal

Frame it as a trade: implicit behaviour removes an entire class of expired-certificate incidents but moves the failure mode from a config line to reachability and storage, which your monitoring and runbooks now have to cover.

## What "automatic HTTPS" actually means Caddy's headline feature is that HTTPS is not a feature you turn on — it is what happens by default when you give a site block an address that looks like a public domain name. The two-line config ``` example.com { reverse_proxy localhost:8080 } ``` is a complete, production-shaped HTTPS reverse proxy. Nothing in it names a certificate, a key, a protocol version or a cipher list. ## The decision Caddy makes from the site address The site address is the input to the whole behaviour. Caddy classifies it: - A **public-looking hostname** (`example.com`, `shop.example.com`) means Caddy should manage a publicly trusted certificate for it via ACME. - **`localhost`, a bare IP address, or a `.localhost` name** cannot be validated by a public CA, so Caddy issues a certificate from its own **internal CA** — a local root it generates and stores itself. The `caddy trust` command installs that root into the machine's trust store so browsers stop complaining. The global option `local_certs` forces this behaviour for every site. - A site block that only has an `http://` scheme, or a config with `auto_https off` in the global options, opts out entirely. `auto_https disable_redirects` keeps the certificates but drops the port-80 redirect — which is what you want when something in front of you already handles that. ## What happens at config load For each managed name Caddy runs an ACME order in the background as the servers come up. It solves the challenge with its own listeners rather than by writing files somewhere for another daemon to serve: - **HTTP-01** answers on port 80 at a well-known path. - **TLS-ALPN-01** answers on port 443 inside the TLS handshake itself. - **DNS-01** is available but needs a DNS-provider module compiled into the binary, so it is not something the stock download can do. Because Caddy owns both listeners, it will fall back from one challenge to the other, and it retries with backoff rather than exiting. That is why a Caddy that cannot get a certificate keeps running and keeps failing handshakes for that name instead of refusing to start. Once issued, the certificate and key are written to Caddy's **data directory** — file-system storage under `$XDG_DATA_HOME/caddy` (typically `~/.local/share/caddy` on Linux, or the service user's equivalent), with certificates filed under a `certificates/` subtree per issuer. This directory is the single most operationally important thing in the whole feature: it is the difference between renewing an existing certificate and asking the CA for a brand-new one every time the process starts. ## Renewal and the redirect Caddy keeps a maintenance loop running for the lifetime of the process. It renews well before expiry — roughly when the last third of the certificate's lifetime is reached — so a short-lived certificate is not a scheduling problem you have to own. It also maintains OCSP staples for the certificates it manages. Separately, automatic HTTPS adds an HTTP-to-HTTPS redirect for every managed name. This is why a config that mentions only `example.com` still binds port 80. ## The preconditions, stated plainly 1. **Public DNS** for the name must resolve to this host. A certificate cannot be validated for a name that does not point at you. 2. **Inbound reachability** on port 80, port 443, or both. A security group or firewall that blocks 80 does not block issuance outright — TLS-ALPN-01 over 443 still works — but blocking both does. 3. **Persistent storage.** If the data directory is ephemeral (the classic case is a container with no volume for it) every restart re-issues, and repeated re-issuance for the same names runs into the CA's rate limits. 4. **The name must be one a public CA will issue for.** Internal-only names, IPs and `localhost` get the internal CA instead — which is a feature for local development, not a failure. ## What this replaces On a conventionally configured web server, the same outcome is a certificate-management client, a renewal timer, a deploy hook that reloads the server, a block of TLS directives, and a redirect server block — five things that can each drift or break independently. Caddy collapses them into the fact that you typed a domain name. The cost is that the behaviour is implicit: when it goes wrong the failure is in logs and in reachability, not in a config line you can point at.

  • The host has port 80 blocked by a firewall but 443 open. Can Caddy still get a certificate?
    Yes. Caddy can complete the TLS-ALPN-01 challenge inside the handshake on port 443, so issuance still works. What you lose is the automatic HTTP-to-HTTPS redirect, which needs port 80 to be bound and reachable. DNS-01 would also avoid both ports, but it needs a DNS-provider module compiled into the binary, so the stock download cannot do it.
  • What changes if the site address is `localhost` instead of `example.com`?
    Caddy will not go to a public CA for a name no public CA can validate. It issues from its own internal CA instead and serves that certificate. Browsers will not trust it until the internal root is installed in the system trust store, which `caddy trust` does. The global option `local_certs` forces this behaviour for all sites, which is useful for offline development.
  • How would you keep Caddy from binding port 80 at all?
    Set `auto_https disable_redirects` in the global options if you still want managed certificates but no redirect server, or write the site address with an explicit `http://` scheme if you want that site served in plaintext only. `auto_https off` disables certificate management and redirects together, which is what you use when something upstream already terminates TLS.

It is less like configuring TLS and more like registering a domain with a landlord who also fits the locks: you say which door is yours, and the keys are cut, installed and re-cut before they wear out.

saying these in an interview costs you the question

  • Thinks you must run a separate ACME client alongside Caddy
  • Says Caddy serves a self-signed certificate for public domains
  • Assumes issuance is impossible when port 80 is blocked
  • Ignores that the data directory must persist across restarts
  • Believes automatic HTTPS also works for a bare IP address

context

open as a page

The Caddyfile is not Caddy's native configuration format. What is, how does the Caddyfile relate to it, and how does a running Caddy instance receive a new configuration?

level: middleimportance: should knowfreq 45%

basics

~20 s

Caddy's native config is JSON. The Caddyfile is human-friendly input that a config adapter converts to that JSON before loading. A running instance takes new config through its admin API, which listens on localhost:2019 and applies changes gracefully without restarting.

open as a page

You run Caddy with automatic HTTPS in containers and scale it from one instance to six behind a network load balancer. Certificate issuance starts failing against the certificate authority's rate limits. What is going wrong and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Each Caddy instance has its own private storage, so all six independently order certificates for the same names — and container restarts lose that storage and re-order again. The fix is one shared, persistent storage backend that every instance reads and writes.

open as a page

For a new internet-facing edge tier, when would you choose Caddy over a more established proxy such as nginx or HAProxy, and what are you giving up by doing so?

level: principalimportance: should knowfreq 44%

basics

~20 s

Choose Caddy when certificate operations dominate the work — many or customer-supplied hostnames, small teams, no PKI runbook — and when a short config is worth more than fine-grained tuning. You give up tuning surface, operational precedent, and dynamic plugins, since third-party modules must be compiled in.

open as a page

Caddy's on-demand TLS obtains a certificate during the TLS handshake for a hostname it has never been configured with. What does that make possible, what is the abuse risk, and what control contains it?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

On-demand TLS lets customers point their own domains at your service with no config change, because Caddy issues per hostname at handshake time. Unrestricted, anyone aiming DNS at you triggers issuance attempts, so it must be gated by an ask endpoint that approves each name.

open as a page