OpenLiteSpeed already does TLS. Certbot already works against it. So why on earth would you run Caddy in front of LSWS as a sidecar — adding a hop, another process to monitor, and a second config file to keep in sync? I’ve been quietly running this exact stack for the last few months, and the honest answer is: most people shouldn’t. But for two specific patterns, this combo eats the alternatives alive.
The “why bother” question
If you’re running a single WordPress site on LSWS, do not do this. Bind LSWS to ports 80/443, point certbot at it, walk away. The cost of a second binary in your chain is real — and you’ll spend more time debugging the seam than you’ll save on anything else.
The two patterns where Caddy-in-front earns its rent:
- Multi-app servers where some apps aren’t WordPress. You’ve got an LSWS box hosting half a dozen WP sites, and now you want to add a Node.js API, a static Astro build, or a Caddy-friendly Go binary. LSWS will do it (extprocessor + scripthandler), but it’s a hassle. Caddy out front, LSWS as one upstream of many, is dramatically simpler.
- Cert provisioning at scale across a fleet. LSWS’s built-in ACME is fine for a few sites. When you cross 20+ vhosts and want zero-touch renewals, Caddy’s automatic HTTPS — including HTTP-01, TLS-ALPN-01, and DNS-01 against Cloudflare — is the lowest-overhead option I’ve found. You add a domain to the Caddyfile, restart Caddy, and a cert appears.
The localhost upstream config that actually works
The first time I wired this up, I did the obvious thing: bind LSWS to 127.0.0.1:8443 still doing TLS, and have Caddy talk to it over HTTPS. That works, but it’s wasteful — you’re TLS-ing twice over a localhost socket. The cleaner setup is to turn TLS off on LSWS for the listener Caddy talks to, bind it to 127.0.0.1:8080 in plain HTTP, and let Caddy do all the TLS termination.
Here’s the Caddyfile entry:
example.com {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto https
}
encode zstd gzip
log {
output file /var/log/caddy/example.com.log {
roll_size 50mb
roll_keep 5
}
}
}
That last header_up X-Forwarded-Proto https is the one that bites people. Without it, WordPress thinks the request is HTTP, generates HTTP asset URLs, and you get the mixed-content warning your users will absolutely notice.
Telling LSWS the truth about who’s connecting
LSWS by default trusts X-Forwarded-For from any source, which is a bug, not a feature. You need to explicitly tell it that only the localhost loopback is a trusted upstream. In /usr/local/lsws/conf/httpd_config.conf:
useIpInProxyHeader 2
accessLog $SERVER_ROOT/logs/access.log {
useServer 0
logHeaders 5
rollingSize 10M
}
The magic value is useIpInProxyHeader 2 — it tells LSWS to trust X-Forwarded-For only when the connection came from a trusted IP, and the trusted IP list defaults to localhost. If you set it to 1, you’ve just trusted the world; that’s how you end up with attackers spoofing IPs in your access logs and bypassing fail2ban rules.
The double-redirect trap
WordPress will sometimes generate a redirect to its own HTTPS canonical URL. If LSWS thinks the request was HTTP (because Caddy stripped TLS), and the canonical URL is HTTPS, you get a 301 → 301 → 301 loop and curl gives up after 50 hops.
Fix is in wp-config.php:
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
Drop that above the require_once ABSPATH line. Now WordPress sees $_SERVER['HTTPS'], generates the right URLs, and the redirect loop disappears.
What this costs you
Two things, both small:
- About 1ms of latency per request — measured, not guessed. The localhost hop is cheap; the JSON parsing for header rewrites is cheaper.
- A second log destination. Your access logs now live in two places. You either teach your log shipper about both, or you turn off LSWS access logging entirely and rely on Caddy’s. I do the latter; LSWS error logs stay on and that’s enough.
When to walk away
If you’re not running multi-app or multi-site-at-scale, this is over-engineering. LSWS handling its own TLS is fine, lswsadmin handles cert rotation through certbot, and you can be done in an afternoon. The Caddy sidecar pattern earns its keep when the seam between your apps is the friction point — which, for me, was when the third Node.js service tried to share a server with WordPress.
The setup above runs without drama on a 4 GB Oracle box hosting 8 sites of mixed stack. It’s not glamorous, but it’s also not the part of the stack I think about anymore — which is the highest compliment I can give to a piece of infrastructure.
Cover photo: Brett Sayles on Pexels.
