To Serve Admin

A practical cookbook for people who run servers

Linux · DevOps · Networking · Homelab · Cloud · Game Servers · AI Infra

§ 1.1 Guide

Caddy Reverse Proxy: Preserve HTTPS Context for Upstream Applications

FIG. 1 — tsa-hero-22-1
Difficulty★☆☆☆☆
Time required5 min
Yieldfully stylized site with all assets loading.

When Caddy terminates HTTPS and proxies to an HTTP upstream, the browser still expects every generated URL to use HTTPS. The failure occurs when the upstream application loses that original scheme and generates http:// asset or API URLs.

Understand the TLS termination boundary

Caddy accepts the client’s HTTPS connection, handles TLS, and forwards the request to the upstream service. That upstream may receive ordinary HTTP on the private network. The distinction is normal; the important part is preserving the original request scheme in the proxy headers.

Caddy’s reverse_proxy handler sets the standard X-Forwarded-Proto header for the upstream. Do not replace it with a hard-coded http value in a header_up directive. A downstream application such as WordPress, Rails, or another framework must then be configured to trust the header at its application boundary.

Recognize the mixed-content symptom

The common symptom is a page that returns HTTP 200 while its CSS, JavaScript, images, or API requests are generated with http://. Browsers then block those requests as mixed content. This is not usually a missing-file problem: the URL scheme is wrong before the request reaches the asset.

Check the page source and browser network panel. If the page is HTTPS but generated resource URLs are HTTP, inspect the proxy-to-upstream scheme path before changing application URLs or clearing caches.

Keep the Caddy proxy configuration narrow

A basic Caddy reverse proxy does not need a custom scheme rewrite:

example.com {
    reverse_proxy 192.0.2.20:80
}

Use the actual private upstream address in your environment. Avoid copying a filesystem path into reverse_proxy; the upstream value is a host and port or a resolvable service name.

If another proxy sits between Caddy and the application, inspect every boundary. The next proxy must preserve X-Forwarded-Proto and pass it to the application runtime. For PHP behind Nginx, that commonly means forwarding the request header through FastCGI; the exact directive belongs in the managed Nginx template rather than an ad-hoc live edit.

Verify the forwarded scheme end to end

First validate Caddy’s public response and the generated URLs:

curl -fsS https://example.com/ -o /tmp/page.html
printf 'http asset URLs: '
grep -o 'http://[^" ]*' /tmp/page.html | wc -l

A zero count is useful evidence, but it is not enough when URLs are generated later by JavaScript. Check the browser network panel as well.

Then inspect the next proxy or application logs for the forwarded scheme. A public response cannot show the private header that Caddy sent upstream, so verify that boundary where it is consumed. If the application still behaves as HTTP, fix its trusted-proxy setting or the next hop’s header forwarding rather than forcing every generated URL manually.

Separate proxy fixes from application fixes

Caddy is responsible for terminating TLS and communicating the original scheme. The upstream application is responsible for trusting that scheme when it builds absolute URLs. Keep those responsibilities separate: first prove the header leaves Caddy, then prove the next layer passes or consumes it, and finally verify the rendered page.

This approach applies to WordPress and other frameworks without turning a Caddy troubleshooting article into a database or plugin configuration guide. Application-specific remediation belongs in a separate article linked from the relevant follow-up.