For a year I tried to standardise our reverse proxies on one tool. The honest outcome was two: Caddy for the dozen internal services, nginx kept at the public edge. This is the reasoning behind the split, not a verdict on which tool is better.
The either/or framing everyone reaches for — Caddy versus nginx — is the wrong question for an estate of any size. The two are not really competing for the same job. Once I stopped trying to crown a winner and started assigning each proxy the role it is genuinely good at, the operational noise dropped. The clearest signal was a thing that simply stopped happening: expired-certificate pages at two in the morning.
The incident class I wanted to delete
Internal services accrete. A monitoring dashboard, a private container registry, a couple of admin panels, an internal API or two — a dozen small applications, each sitting behind its own TLS certificate. We managed those with certbot and cron, and roughly once a quarter one would slip: a renewal hook that failed silently, a service that kept serving the old certificate after renewal, a DNS-01 challenge that broke when a provider changed its API. None of these were disasters. All of them were interruptions, and they arrived without warning at whatever hour the certificate happened to expire. These are exactly the incidents that inflate your change-failure rate and mean time to recovery without ever being interesting enough to justify the fix getting prioritised.
Free · 4 minutes
Is your engineering team shipping safely, or quietly accumulating risk?
Fourteen questions on how work gets from idea to production — cadence, testing, rollback, and the key-person risk in your delivery. Banded finding on screen, full sheet by email.
What Caddy actually removed
Caddy’s headline feature is automatic HTTPS, and for internal services it earns the description. For a public name it obtains and renews a Let’s Encrypt certificate on its own. For internal-only names it does not need a public authority at all: tls internal issues from Caddy’s own locally-trusted certificate authority, and it rotates those leaf certificates on a short cycle — twelve hours by default — with no cron, no hook, and nobody touching them. Certificate expiry stopped being a category of work.
# /etc/caddy/Caddyfile — a dozen internal apps, one block each
grafana.internal.example {
tls internal # Caddy's own locally-trusted CA
reverse_proxy localhost:3000
}
registry.internal.example {
tls internal
reverse_proxy localhost:5000
}
# A service that DOES need a public certificate:
status.example.com {
reverse_proxy localhost:8080 # automatic HTTPS via Let's Encrypt
}
That whole file is the configuration. Automatic HTTPS is on by default, so there is no separate SSL block, no certbot, no renewal timer. A caddy reload applies changes with no dropped connections. For a fleet of low-importance internal apps where the entire operational goal is to eliminate toil, this is close to ideal, and I moved all dozen across without regret.
Why nginx stayed at the edge
The public edge is where I want explicit control, and control is precisely what Caddy trades away for its simplicity. At the boundary I run response caching, rate limiting and mutual TLS, and nginx exposes each of those as a first-class, exhaustively documented primitive that I can tune by hand.
# /etc/nginx/conf.d/edge.conf — explicit control at the boundary
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
proxy_cache_path /var/cache/nginx keys_zone=edge:50m max_size=2g inactive=10m;
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Mutual TLS for a named partner integration
ssl_client_certificate /etc/nginx/mtls/partner-ca.pem;
ssl_verify_client on;
location /v1/ {
limit_req zone=api burst=40 nodelay;
proxy_cache edge;
proxy_pass http://127.0.0.1:8080;
}
}
Every one of those directives has years of production behaviour behind it, and when I need to reason about a caching edge case or a burst policy at three in the morning, the answer is a well-worn one. The mutual TLS block in particular — verifying a partner’s client certificate before their traffic reaches an application — is somewhere I deliberately want the mature, boring implementation rather than the clever one. The edge is also where I terminate and check credentials, so it sits naturally alongside how we handle API keys and JWT validation. Caddy can do rate limiting and mTLS too, but with a smaller vocabulary and a shallower well of prior art to draw on when something behaves oddly.
The honest losses
Caddy is not free of cost, and pretending otherwise is how you end up disappointed. The ecosystem is smaller: fewer answers when you search a specific error, fewer third-party modules, and some of the modules that do exist require compiling a custom binary with xcaddy rather than dropping a shared object into a directory. The Caddyfile is genuinely elegant until you need something unusual, at which point you drop down to Caddy’s underlying JSON configuration, which is powerful, complete, and considerably less pleasant to hand-write. And a single Go binary that manages its own certificates is a joy right up until you hit a bug in that management — at which point far fewer engineers have hit it before you than would have with nginx and certbot.
These are real trade-offs, but they land almost entirely on the edge, not on the internal fleet. For a dozen internal apps that each do one thing, the small ecosystem never bites, because I never ask Caddy to do anything unusual. For the public boundary, where the unusual is routine, the shallower well is exactly the reason I kept nginx.
The rule I use now
The split comes down to a single question I ask of each proxy: do I need this to be predictable, or do I need it to be controllable? An internal fleet — high count, low individual importance, where the win is deleting a whole class of certificate toil — wants predictability, and Caddy delivers it in a handful of lines. A public edge — small count, high importance, where the requirement is fine-grained control over caching, limits and client authentication — wants controllability, and nginx still owns that ground. Running two proxies is not the failure to decide. It is the decision.
Build and rescue work
Hands-on delivery of this kind is handled by Sixteen Pillars Studio.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Can you trust the architecture you have?
Architecture diagrams rarely show the reality of how systems actually operate. An independent review establishes what is really there.