Caddy is the only web server that automatically provisions and renews TLS certificates for your sites, making HTTPS a breeze.
Let’s see it in action. Suppose we have a domain, example.com, and we want to serve a simple static HTML file over HTTPS.
First, create a basic index.html:
<!DOCTYPE html>
<html>
<head>
<title>Hello, HTTPS!</title>
</head>
<body>
<h1>This site is served over HTTPS by Caddy!</h1>
</body>
</html>
Now, create a Caddyfile to configure Caddy. The simplest Caddyfile for our domain would look like this:
example.com {
root * .
file_server
}
This tells Caddy to serve files from the current directory (.) for the domain example.com using its file_server directive. Caddy will automatically detect that example.com is a public domain and attempt to obtain a TLS certificate from Let’s Encrypt.
To run Caddy, you can download a binary or use Docker. Using Docker is often the easiest way:
docker run -p 80:80 -p 443:443 -v $PWD/Caddyfile:/etc/caddy/Caddyfile -v $PWD/html:/srv \
caddy:latest caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
Make sure you have a directory named html in your current directory and place your index.html inside it. The command maps port 80 and 443 from the host to the container, mounts your Caddyfile and your static files directory.
When you access https://example.com in your browser, Caddy will:
- Receive the request: The request for
https://example.comarrives on port 443. - Check for a certificate: Caddy looks for a TLS certificate for
example.com. If none exists or it’s close to expiring, it proceeds to obtain one. - Initiate ACME challenge: Caddy uses the ACME protocol (the protocol Let’s Encrypt uses) to prove to Let’s Encrypt that it controls
example.com. The most common challenge type for web servers is the HTTP-01 challenge. - Respond to the challenge: Caddy temporarily serves a specific file at a specific URL (
/.well-known/acme-challenge/...) that Let’s Encrypt’s servers will request over HTTP. This proves domain ownership. - Obtain and install the certificate: If the challenge is successful, Let’s Encrypt issues a certificate. Caddy automatically installs this certificate and uses it to serve your site over HTTPS.
- Serve the content: Caddy then serves your
index.htmlfile.
The magic here is that you didn’t have to manually generate CSRs, upload them, or configure complex TLS settings. Caddy handles all of this automatically and silently in the background. It also automatically renews certificates before they expire.
The Caddyfile is Caddy’s primary configuration format. It’s designed to be human-readable and simple. Directives like root, file_server, reverse_proxy, log, tls, etc., allow you to control various aspects of Caddy’s behavior. When you specify a domain name as the first element in a block, Caddy assumes you want to configure that site, and if it’s a public domain, it will attempt to manage its TLS certificate.
The root directive sets the base directory for static files. file_server enables the static file server. For dynamic content or APIs, you’d use reverse_proxy to forward requests to another backend service.
example.com {
reverse_proxy localhost:8080
}
This Caddyfile would proxy all requests for example.com to a service running on localhost:8080, and Caddy would still automatically manage the TLS certificate.
The tls directive can be used to explicitly configure TLS settings, though for automatic Let’s Encrypt integration, it’s often not needed. You can use it to specify alternate certificate issuers or provide your own certificates. For example:
example.com {
tls your-email@example.com # Optional: provide your email for Let's Encrypt
root * .
file_server
}
Providing an email address is good practice as Let’s Encrypt uses it to notify you of important information, like certificate expiry issues.
When Caddy obtains a certificate, it stores it in a local directory. By default, this is usually within Caddy’s data directory, which can be configured. This local storage means that subsequent requests for the same domain don’t require another ACME challenge if the certificate is still valid. Caddy checks the expiry date and automatically initiates renewal approximately 30 days before the certificate expires, ensuring uninterrupted HTTPS.
The most surprising thing about Caddy’s automatic HTTPS is how it handles internal or private domains. If you try to configure my-internal-server.local with Caddy, it won’t try to get a Let’s Encrypt certificate because those are not publicly resolvable domains. Instead, Caddy will automatically provision a self-signed certificate for that domain. This means you get HTTPS immediately, even for hosts that aren’t publicly accessible, without any manual configuration. Browsers will show a warning because the certificate isn’t trusted by a public CA, but the connection is still encrypted.
The next step in mastering Caddy is understanding its extensive plugin ecosystem and how to build custom configurations with JSON.