Setting Up Local HTTPS with Caddy
At some point during local development, you'll run into situations where HTTPS is required. OAuth redirect URLs that only accept HTTPS, cookies with the Secure flag, or service workers that refuse to run on plain HTTP. http://localhost won't cut it anymore.
You could set up Nginx as a reverse proxy with a self-signed certificate, but that involves generating certs with OpenSSL, configuring SSL paths, and dealing with browser warnings every time. Caddy makes this much simpler.
What is Caddy
Caddy is a web server written in Go. Its defining feature is automatic HTTPS. In production, it uses Let's Encrypt for certificates. Locally, it creates its own CA (Certificate Authority) and issues certificates from it. No OpenSSL commands, no manual certificate files.
The configuration file — called a Caddyfile — is also straightforward. Compared to Nginx config, there's significantly less boilerplate.
Installation
On macOS, install via Homebrew:
brew install caddy
Verify with caddy version.
Trust the Root Certificate
For Caddy's local HTTPS to work without browser warnings, you need to register its CA root certificate as trusted on your system. This is what prevents the "Your connection is not private" screen when accessing https://localhost.
caddy trust
This creates a local CA and adds its root certificate to the system keychain. On macOS, you may be prompted for your keychain password. You only need to do this once.
Writing a Caddyfile
Create a Caddyfile in your project root. For example, if you have a dev server running on port 3000 and want to access it over HTTPS:
localhost {
reverse_proxy localhost:3000
}
That's it. With Nginx, you'd need a server block, ssl_certificate paths, proxy_pass directives, and more. Caddy applies HTTPS automatically when you specify a domain.
If you want to use a custom domain, add it to /etc/hosts and reference it in the Caddyfile:
# Add to /etc/hosts
127.0.0.1 myapp.local
myapp.local {
reverse_proxy localhost:3000
}
Running multiple services is just as easy:
api.localhost {
reverse_proxy localhost:8080
}
app.localhost {
reverse_proxy localhost:3000
}
Running Caddy
Run it from the directory containing your Caddyfile:
sudo caddy run --config ./Caddyfile
sudo is needed because HTTPS uses port 443, and ports below 1024 require root privileges.
Once running, you'll see logs in the terminal. Open https://localhost in your browser and the connection will be secured with no certificate warnings.
To run it in the background, use start instead of run:
sudo caddy start --config ./Caddyfile
To stop a background instance:
sudo caddy stop
When Caddy Doesn't Shut Down Cleanly
Sometimes Caddy doesn't terminate properly, leaving port 443 occupied. You'll see an error like this:
Error: loading initial config: loading new config: http app module: start: listening on :443: listen tcp :443: bind: address already in use
Force-kill the process:
sudo pkill -9 caddy
If that doesn't help, check what's holding port 443:
sudo lsof -i :443
Find the PID and kill it directly:
sudo kill -9 <PID>
Summary
When you need local HTTPS, Caddy handles certificate generation, system trust registration, and reverse proxying all at once. The whole setup comes down to three steps:
brew install caddy— installcaddy trust— trust the root certificate (once)sudo caddy run --config ./Caddyfile— run
No manually creating certificate files, no specifying SSL paths. For local development, Caddy is noticeably easier than Nginx.