A proxy is a server that sits between your visitors and your site. This chapter is for sites that sit behind one that handles https for them: a Cloudflare Tunnel, Pangolin, the Cloudflare proxy, or a proxy of your own such as nginx. At the end your site answers at your domain through the proxy, and you know which line in .env to change to move it between setups.
You need a site that already runs on a server, or at least the files from Run it on a server. As there, example.com stands for your domain. If you have one server, a domain and nothing in front of it, you do not need this chapter. The setup in the previous chapter is simpler.
Cloudflare and Pangolin rename things in their dashboards from time to time. The steps below describe what each step does, so that you can find it even when the name has changed.
One line in .env, COMPOSE_PROFILES, picks which Caddy runs in front of the site. In https mode Caddy gets its own certificate. In proxy mode Caddy serves plain http inside your server and leaves https to the proxy. A tunnel connector is one more profile beside it, and it reaches Caddy over Docker's own network, so nothing is published to the internet.
| Setup | COMPOSE_PROFILES | What runs | Ports to open |
|---|---|---|---|
| Caddy gets the certificate (the previous chapter) | https | caddy-https | 80 and 443 |
| Cloudflare Tunnel | proxy,cloudflare | caddy-proxy and cloudflared | none |
| Pangolin | proxy,pangolin | caddy-proxy and newt | none |
| Cloudflare proxy, no tunnel | https | caddy-https | 80 and 443 |
| Another proxy | proxy | caddy-proxy | one port, on the address in CMS_BIND |
Whatever the setup, CMS_SITE_URL in .env is the address your visitors type, for example https://example.com. It is not the address of your server or of Docker. The site uses it to:
X-Forwarded-* headers from Caddy can be trusted, which is how the site learns that the visitor used https;The scaffold's .env starts with http://127.0.0.1:4321, which is right on your own computer and wrong behind a proxy. If you forget to change it, sign-in fails; Troubleshooting shows how to recognise that.
A Cloudflare Tunnel needs your domain's DNS to be on Cloudflare. A small program, cloudflared, runs next to the site and holds a connection open to Cloudflare, which sends your visitors down it. Compose runs that program for you.
my-site.--token.caddy-proxy:80. Cloudflare creates the DNS record for you..env. CMS_AUTH_SECRET stays as it is.CMS_SITE_URL=https://example.com
COMPOSE_PROFILES=proxy,cloudflare
CLOUDFLARE_TUNNEL_TOKEN=paste-the-token-heredocker compose up -d --builddocker compose logs --tail 20 cloudflaredThen open your domain. On a new site, create your admin account as in Run it on a server. No firewall ports are needed here beyond SSH.
Pangolin is a self-hosted proxy. A small connector, Newt, runs next to your site and connects out to your Pangolin, which serves your domain and handles the certificate.
http, address caddy-proxy, port 80..env:CMS_SITE_URL=https://example.com
COMPOSE_PROFILES=proxy,pangolin
PANGOLIN_ENDPOINT=https://pangolin.example.com
NEWT_ID=paste-the-id-here
NEWT_SECRET=paste-the-secret-heredocker compose up -d --builddocker compose logs --tail 20 newtHere your domain's DNS record points at your server and the cloud next to it is orange, so visitors reach Cloudflare first and Cloudflare reaches your server. This is the https setup from the previous chapter with three changes. Caddy still gets a real certificate, which Cloudflare checks.
COMPOSE_PROFILES=https. If Caddy cannot get its certificate while the cloud is orange, set the record to DNS only until it has one, then turn the proxy back on.CF-Connecting-IP. Add this to .env:CMS_CLIENT_IP_HEADERS=CF-Connecting-IP.env:printf 'CMS_TRUSTED_PROXIES="private_ranges %s"\n' "$({ curl -s https://www.cloudflare.com/ips-v4; echo; curl -s https://www.cloudflare.com/ips-v6; } | tr '\n' ' ')" >> .envprivate_ranges keeps Docker's own network trusted. If you run the command again later, delete the old CMS_TRUSTED_PROXIES line from .env first, and look at Cloudflare's list now and then.
docker compose up -dIf you already run nginx, Traefik, HAProxy or another Caddy, let it handle https and give it the site over plain http. Use COMPOSE_PROFILES=proxy. Caddy then publishes the site's port, 4321 unless you chose another, on the address in CMS_BIND. That address is 127.0.0.1 by default, so only the machine itself can reach it, which suits a proxy on the same machine. Point that proxy at http://127.0.0.1:4321.
CMS_SITE_URL=https://example.com
COMPOSE_PROFILES=proxyYour proxy has to pass on the original Host header and set X-Forwarded-Proto: https and X-Forwarded-For. The site depends on them to know that the visitor used https.
When the proxy runs on another machine, set CMS_BIND to the server's address on the private network between the two, such as 10.0.0.5. Do not use 0.0.0.0. That publishes the port on every network the server is on, the internet included, and ufw does not stop Docker's published ports.
CMS_BIND=10.0.0.5Caddy believes forwarded headers only from private addresses by default. If your proxy connects from a public address, add it to CMS_TRUSTED_PROXIES, keeping private_ranges in the list:
CMS_TRUSTED_PROXIES="private_ranges 198.51.100.7"To move a running site to another setup, change COMPOSE_PROFILES in .env and add the settings the new setup needs. Then remove the old Caddy and connectors by name and start the new set:
docker compose rm -sf caddy-https caddy-proxy cloudflared newt
docker compose up -dBoth steps are needed. Compose looks only at the profiles that are switched on. docker compose up -d alone starts what the new value lists and leaves the old Caddy running, still holding its ports. docker compose down removes only what the current value lists. The first command removes the old ones by name, and it does not mind if one of the names is not running.
Your data is not touched. Compose does restart the site itself, because it reads .env, so expect the site to be down for about twenty seconds while it starts and Caddy waits for it.
To stop everything the site runs, in any setup, use the following. It keeps the data; add -v to delete that too.
docker compose --profile '*' downGoing live covers email, payments and a checklist for before real visitors arrive.