Isolated Multi-User Caddy Setup with Reverse Proxy

Hey everyone,

First of all, thank you for the great software and excellent documentation. I’m struggling with configuring the reverse proxy correctly and would appreciate some guidance.

1. The problem I’m having:

I’m using Ubuntu 24.04 LTS on DigitalOcean, without Docker. The server stack is: Caddy + FrankenPHP + MySQL.

Each system user has their own account and isolated webspace. To maintain full separation, I run a separate Caddy instance per user, in addition to a single front-facing Caddy instance.

  • Front Caddy server (system-wide): handles TLS and forwards requests to backends
  • Back Caddy servers (one per user): handle actual site serving

These backends are not publicly exposed and should only be accessible internally.

Example users: foo, bar

2. Error messages and/or full log output:

Problem 1: REQUEST_SCHEME is http instead of https

When visiting https://foo.com, the value of $_SERVER['REQUEST_SCHEME'] is http.
I assume this is because the front → back request is unencrypted. I tried setting reverse_proxy https://localhost:9011, but that broke the site (likely due to missing SSL certs on the backend).

While I know internal traffic doesn’t strictly need encryption, the incorrect scheme causes issues in app logic.

Question:
How can I preserve https in the backend, or otherwise correctly inform PHP that the original request was HTTPS?


Problem 2: Port conflicts and exposure

Initially, both front and back Caddy servers tried to use ports 80/443, causing conflicts. I resolved this by running back servers on alternate ports (9010, 9011, etc.).

Question:
Is this the correct approach?
How can I ensure backends are not accessible via <server_ip>:9010 from the public internet?


3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

Caddy:

apt update && apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | tee /usr/share/keyrings/caddy-stable-archive-keyring.gpg >/dev/null
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

FrankenPHP:

curl -fsSL https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
frankenphp --version

a. System environment:

  • Caddy: v2.10.0 (h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=)
  • FrankenPHP: latest
  • System: 2 GB RAM / 1 vCPU / Ubuntu 24.04 LTS

b. Command:

Front server:

systemctl start caddy

Back servers: run with individual systemd units like:

[Unit]
Description=FrankenPHP instance for foo
After=network.target

[Service]
User=foo
Group=foo
WorkingDirectory=/home/foo
UMask=007
RuntimeDirectory=frankenphp
RuntimeDirectoryMode=0755
ExecStart=/usr/local/bin/frankenphp run --config /home/foo/Caddyfile
Restart=on-failure
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

c. Service/unit/compose file:

Not relevant.

d. My complete Caddy config:

Front Caddyfile (/etc/caddy/Caddyfile):

# User "foo"
foo.com {
    reverse_proxy http://localhost:9010
}

# User "bar"
bar.com {
    reverse_proxy http://localhost:9020
}

Back Caddyfile for foo (/home/foo/Caddyfile):

{
    admin off
    http_port 9010
    https_port 9011

    servers {
        trusted_proxies static private_ranges
    }
}

http://foo.com {
    encode zstd br gzip
    root * /home/foo/www/foo.com/public
    php_server
}

Back Caddyfile for bar (/home/bar/Caddyfile):

{
    admin off
    http_port 9020
    https_port 9021

    servers {
        trusted_proxies static private_ranges
    }
}

http://bar.com {
    encode zstd br gzip
    root * /home/bar/www/bar.com/public
    php_server
}

Any insights are greatly appreciated. I’m especially looking for advice on how to:

  1. Ensure HTTPS is properly reflected in the backend.
  2. Keep backend ports inaccessible from the public.

Thanks in advance! :slightly_smiling_face:

Caddy automatically sends the following X-Forwarded headers to backend servers:

The one you want is X-Forwarded-Proto, which contains the original scheme.

So instead of relying on $_SERVER['REQUEST_SCHEME'], try using $_SERVER['HTTP_X_FORWARDED_PROTO']. That should give you the correct https value from the client’s request.

Running the backend Caddy instances on ports like 9010, 9011, etc., is a common approach. To keep them from being publicly accessible, you have a couple of options:

You can, for example, configure a firewall on your server to block incoming traffic on those ports from the public internet. Another option would be having the backend Caddy instance listening on 127.0.0.1:9010 instead of 0.0.0.0:9010. This restricts access to localhost only.

Both options work well, it just depends on your setup and preferences.

You can check the following if you decide to bind the back-end servers to 127.0.0.1

3 Likes

@timelordx thank you for your reply. I have configured trusted proxies in my Symfony app now. Also I must have missed the bind option. Everything works perfectly now. Thanks a lot!

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.