Docker - Multiple containers with SSL

1. Caddy version (caddy version):

latest

2. How I run Caddy:

DNS record from Cloudflare to server IP.
Docker run command directly on Ubuntu host machine.

a. System environment:

Docker on Ubuntu host

b. Command:

docker run -d -p 80:80 -p 443:443 \
    -v /srv/caddy_data:/data \
    -v /srv/caddy_config:/config \
    caddy caddy file-server --domain subdomain.domain.com

c. Service/unit/compose file:

No compose

3. The problem I’m having:

I’m a bit lost as to how I should tackle this issue.
Found my way here to the forums… Hopefully you guys can enlighten me.

I’m trying to set up multiple Docker containers, using Caddy, and having all these containers have the automatic SSL certificate.
Getting the first container up and running is simple, of course, but when I try to spin up more containers, I’m running into issues, because the ports on my host are already “in use” by the first container.
This means I cannot have more than one Caddy container running at a time, and ultimately I’m not able to do the setup where I have a container for each of my sub-domains.

4. Error messages and/or full log output:

docker: Error response from daemon: driver failed programming external connectivity on endpoint container_name (9760f812eab): Bind for 0.0.0.0:443 failed: port is already allocated.

5. What I already tried:

I tried changing the ports used for Docker to connect to something like -p 8080:80 -p 4430:443.
This did not work, as I assume Caddy will only try the ACME challenge on specific ports 80 and 443.

6. Links to relevant resources:

All the articles I found points me to docker-compose.
I’m not sure if that the only way I can solve this, as I’m not yet as experienced with Caddy nor Docker in general.

Yep. Caddy must be accessible on ports 80 and 443 for Let’s Encrypt to verify the ACME challenge.

Right now, you’re running Caddy with the caddy file-server command. That’s too simple for what you’re wanting to do. Instead, you’ll need to run with a Caddyfile. It might look like this:

subdomain.domain.com {
	reverse_proxy your-other-container:8080
}

another.domain.com {
	reverse_proxy another-container:8080
}

You don’t need to run more than one instance of Caddy, you should only run one, and have it proxy to your other services by hostname.

1 Like

Thanks for your reply.
Seems like I totally need to utilize a Caddyfile then.

So what should these containers be, if I’m just looking to have static websites deployed?
Should I run a caddy file-server, like I did in my first post, for all the websites/subdomains, but without having port 80 and 443 “bound” to them, and then have 1 “main” Caddy container as a reverse-proxy to all the other container names and ports?

I might be totally off, but I’m not sure.

All I’m trying to do is serve a couple of static websites from some folders on the host, and I assumed putting each site in a Docker container (using Caddy) was the way to go…

For serving static sites, use the file_server directive (paired with the root directive to tell Caddy where to look):

You just need to mount those sites in different volumes in Caddy, and you can serve both with one container.

1 Like

Thanks, so when you said to only use one Caddy container, you did mean absolutely only one, I think I get it.

Spin up a Caddy docker container, and mount the folders on my host system, where I have my websites saved.
Then, inside the Caddy container make a Caddyfile.

The Caddyfile can work as a reverse proxy, and make links to other containers (node containers for example), but also work as a direct server of static content using “file_server” instead of “reverse_proxy”.
Caddyfile can also work as a php webserver by using “php_fastcgi” along with folder path.
All these examples are posted here: Common Caddyfile Patterns — Caddy Documentation

Please correct me if I’m wrong.

I’ll be using the Caddy container to serve a few php sites, so can you elaborate what is meant with “With a PHP FastCGI service running…”
Do I need to configure anything for this to work? Or just copy the code and place in my Caddyfile?

Well it depends. If you need to run other apps that aren’t just static sites, that need proxying to, then you’d run a container for each of those other apps and let Caddy proxy to them. But yeah for simple static sites, Caddy can do it on its own.

To run a PHP app, you’ll need to have a php-fpm container running which actually executes the PHP code. See the wiki for various examples:

Caddy doesn’t run the PHP code itself, all it does is act as a fastcgi client which is the protocol used to communicate with php-fpm. It tells php-fpm what request came in and passes along the relevant details to have it run correctly.

1 Like

First of all, thank you so much for your help so far!
With your help, I managed to solve my initial question, however I’m still stuck at getting the php side of things up and running.

I created my Caddyfile, and it looks like this:

 sub1.domain.com {
         root * /var/www/sub1.domain.com
         file_server
 }
 sub2.domain.com {
         root * /var/www/sub2.domain.com
         file_server
 }
 php.domain.com {
         root * /var/www/php.domain.com
         php_fastcgi * phpfpm:9000
}

I run the caddy container with this command:

docker run -d -p 80:80 -p 443:443  --name caddy \
    -v $PWD/Caddyfile:/etc/caddy/Caddyfile \
    -v /var/www:/var/www \
    -v caddy_data:/data \
    -v /srv/caddy_config:/config \
    caddy

Both sub1.domain.com and sub2.domain.com works perfectly as static sites, with a nice https connection.
However, my php subdomain is still not playing par…

I installed this container from docker hub: Docker Hub
Ran it using:

docker run --name phpfpm bitnami/php-fpm

I assume that’s what you mean when you said this, right?

The installation on their docker page says to pass a volume with the app into the container, but this got me confused. I’m supposed to just have the php-fpm do it’s own thing, and pass any directories to Caddy right? Or no?
I tried passing the php volume into the container like this:

docker run -it --name phpfpm -v /var/www/php.domain.com:/app bitnami/php-fpm

This still did not work…

I had to go with this “bitname” fork, because the official php container (Docker Hub) had me even more confused.
They do list something with “fpm” in their supported tags, but I’m still not sure. Furthermore they also list passing a volume with the “app” to the container.
Again, I thought we’re only supposed to pass the directory to Caddy?
Because then I can run multiple sites, with different folders, off of the same php-fpm container. Or do I need a new php-fpm container for each project?

I do realize these are a lot of questions, but I’m really new to this, and I hope you wont mind helping me on my way.
Thank you in advance.

If you’re running more than one container on a single machine, you’ll have a much better time using docker-compose. It’s basically a way to turn your docker commands into a configuration file. You can use the example at the bottom of Docker Hub as a starting point. See the official docs for that:

The gist is that you’ll just make a docker-compose.yml then run docker-compose up -d and your whole stack will be spun up. Then you can do docker-compose down to shut everything down all at once. And there’s a bunch of other helpful tools there. Google is your friend.

You’ll definitely want to use the Docker Hub image, specifically the fpm variant. Depends what PHP version you want to target, but you’ll probably use like php:7.4-fpm-alpine.

The important thing is that you use the same directory path inside the container for both, because Caddy will tell the php-fpm container which file path to look for the PHP script to execute. You can mount /var/www in your php-fpm container as well (or just /var/www/php.domain.com, whatever)

Also, you’ll need a file_server directive for your PHP site as well, because Caddy will likely still need to serve static files (JS, CSS, images, etc) as well.

It might be helpful to look for nginx+php docker examples, since the same concepts apply for Caddy, just with all the nginx-specific bits swapped out with Caddy. There’s just more content out there about nginx since it’s older and more popular (even though I think Caddy is much better and easier to use!)

1 Like

Francis… I’m very delighted to not only wish you a Merry Christmas, but also to give my deepest appreciation for your help and consistent efforts to get a newcomer, like me, up and running in the best possible way.

Truth to be told, I’ve been avoiding docker-compose, because I found the yaml files frightening, scary, and not very welcoming for absolute beginners.
This was the same reason why I didn’t get “hands-on” with the Caddyfile initially, and only looked into it after you persuaded me.
Caddyfile wasn’t scary after your example and a link to the documentation, and would you believe, docker-compose was the exact same story - not at all frightening after a few good examples and your comforting “push” in that direction.

Thanks to you, I created my first Caddyfile, and thanks to you I now created my first docker-compose file too.
And you know what? It was exactly what I needed!

I now have all my subdomains running static pages, as well as the php-subdomain running a fastcgi link to my php-fpm docker container.
It all works, and I can make changes to the setup any time, and just do a “docker-compose up -d” and not have to kill/remove all the previous containers!

If it haven’t become clear already, I’m extremely thankful for your help, and the way you help is very good, because it forces people to go out and seek information for themselves, in order to actually learn what they’re doing, instead of you just giving code and ending in a “monkey see, monkey do” kind of situation.

So yeah, thank you for everything, I’ll leave you with these final words:

I couldn’t agree more.
After looking at various nginx examples, which accomplishes the same as I just did, I’m amazed at how efficient Caddy does the work.
So much cleaner, so much less code, and jam-packed with awesome features. Getting an SSL cert shouldn’t be such a hassle, and it’s not with Caddy!

I’m glad I got onboard of the Caddy train, and I can’t wait to see where it goes from here. :caddy: :heart:

4 Likes

:grin:

Thanks you for the kind words, Merry Christmas to you too! :christmas_tree:

1 Like

Hello again Francis,

I might have been a bit too quick on my feet with the last message, as it seems there’s still one last issue, before my php webserver is running correctly.

While the site does indeed work for displaying a php-page, when I copied over my entire site, I noticed that it would serve all requests to the /index.php page!
Here’s what I’m working with:

docker-compose:

version: "3"

services:
  phpfpm:
    image: php:7.4-fpm-alpine
    container_name: phpfpm
    volumes:
      - /var/www:/var/www
    ports:
      - 9000:9000

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/www:/var/www:ro
      - /srv/caddy_data:/data
      - /srv/caddy_config:/config
      - /home/fox/Caddyfile:/etc/caddy/Caddyfile

Caddyfile:

# Main Site
sub.website.com {
        root * /var/www/sub.website.com
        php_fastcgi phpfpm:9000
        file_server
}

Like I said, the site loads the frontpage, but from the console I can see multiple requests like this:

phpfpm     | 172.24.0.5 -  25/Dec/2020:20:55:12 +0000 "GET /index.php" 200
phpfpm     | 172.24.0.5 -  25/Dec/2020:20:55:12 +0000 "GET /index.php" 200
phpfpm     | 172.24.0.5 -  25/Dec/2020:20:55:13 +0000 "GET /index.php" 200
phpfpm     | 172.24.0.5 -  25/Dec/2020:20:55:13 +0000 "GET /index.php" 200

When I try to go to another page, on the php site, it will print the same stuff in the console.
So it’s always sending a request for the index.php page, no matter what GET request it should actually send.

I assume the issue lies within my Caddyfile.
Specifically in either the root or php_fastcgi directive.
My main suspicion is on the wildcard matcher, but I’m not sure what other matcher I should use…
All examples in the documentation, regarding php directive, utilizes the wildcard matcher, and while it was fine for displaying a simple “phpinfo() page”, it does not seem like this is the way to go for real sites, with different paths folders and links.

Please help me shed some light on it.
As always I appreciate your help a lot.

Yeah, using index.php as the fallback for all requests to unknown files is very typical for modern PHP apps, so that’s the default behaviour of the expanded form of php_fastcgi. You can see this in the expanded form, where it uses try_files to do a redirect if the request path does not map to a file on disk.

Probably not the best resource (I wouldn’t recommend using this code directly) on short notice, but here’s an explanation of how an index router typically works

So if that doesn’t match your expectations, how is your PHP app structured? Are you using a framework? What request paths do you expect to do what?

Awesome info, definitely helped me shed some light on things!

I am indeed using a php framework, and upon further investigation it is structured on index routing.
The framework is originally made for Apache though, but using the tip you gave me earlier (didn’t forget), I went ahead and looked for nginx examples.

Turns out you do in fact need some extra configuration for running this framework.
Here’s the configuration needed.

location / {
  try_files $uri $uri/ =404;

   if (!-e $request_filename)
    { 
        rewrite ^/admin/(.*)?$ /admin/index.php?a=$1 last;
        rewrite ^/(.*)$ /index.php?a=$1 last;
        break; 
    }
}

Unfortunately I’m having a very hard time converting this to Caddyfile markup.
Only having learnt the language a few days ago, and now having to also learn nginx markup to convert, seems like a big chunk to chew… Hoping you can help me out a bit.

Ah interesting, it uses a nonstandard approach for the index.php in that it takes a ?a= param for the request path. Frameworks like Laravel and others just take the request path as a direct suffix to index.php, like /index.php/foo for a request to /foo.

I think you can add this to your Caddyfile to do the rewrites properly:

handle /admin/* {
    rewrite /admin/index.php?a={path}&{query}
}
handle {
    try_files {path} {path}/ /index.php?a={path}&{query}
}

Just put this before your php_fastcgi. I think it should work but we’ll see.

Thanks.

I added the configuration like you said, but unfortunately it did not resolve the issue.
From the console I see the first request returning a http 200, and from there on it’s giving 302 for the next couple of requests (loading assets I presume).

Hmm, well I recommend you make the requests with curl -v to get full detail of what the response looks like.

Also you can enable the debug global option to get more details about the rewrites happening in your logs. You can find the logs with docker-compose logs caddy

I’m afraid I won’t be able to help more without more detail about what’s going on.

Thanks, I enabled the debugging for the domain.
I’m trying to access the URI “/servers”.

phpfpm    | 172.29.0.2 -  27/Dec/2020:15:29:31 +0000 "GET /index.php" 302
caddy     | {"level":"info","ts":1609082971.3080006,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_addr":"162.158.154.108:46350","proto":"HTTP/1.1","method":"GET","host":"php.domain.com","uri":"/servers","headers":{"Accept-Language":["en-GB,en;q=0.5"],"Upgrade-Insecure-Requests":["1"],"Cf-Request-Id":["07466abc7a0000408f7ead2000000001"],"Cdn-Loop":["cloudflare"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"],"Cf-Ipcountry":["GB"],"Cf-Ray":["608413da5c0d408f-LHR"],"X-Forwarded-Proto":["https"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Referer":["https://php.domain.com/"],"Accept-Encoding":["gzip"],"X-Forwarded-For":["x.x.x.x"],"Cookie":["_ga=GA1.2.678239338.1604131132; __cfduid=debfee6079a0d559f4c94455c399f01921608738744; cookieconsent_status=dismiss; PHPSESSID=7f388b4af221c6a4b4bffcf910602f61"],"Cf-Visitor":["{\"scheme\":\"https\"}"],"Cf-Connecting-Ip":["x.x.x.x"],"Connection":["Keep-Alive"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"","proto_mutual":true,"server_name":"php.domain.com"}},"common_log":"162.158.154.108 - - [27/Dec/2020:15:29:31 +0000] \"GET /servers HTTP/1.1\" 302 20","duration":0.020263877,"size":20,"status":302,"resp_headers":{"Server":["Caddy"],"Status":["302 Found"],"Expires":["Thu, 19 Nov 1981 08:52:00 GMT"],"Location":["https://php.domain.com"],"X-Powered-By":["PHP/7.4.13"],"Vary":["Accept-Encoding"],"Pragma":["no-cache"],"Content-Encoding":["gzip"],"Cache-Control":["no-store, no-cache, must-revalidate"],"Content-Type":["text/html; charset=UTF-8"]}}

After this request, it immediately sends another for the root with no URI, and returns the correct http response 200.

phpfpm    | 172.29.0.3 -  27/Dec/2020:15:41:03 +0000 "GET /index.php" 200
caddy     | {"level":"info","ts":1609083663.297698,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_addr":"162.158.158.146:35758","proto":"HTTP/1.1","method":"GET","host":"php.domain.com","uri":"/","headers":{"Cf-Visitor":["{\"scheme\":\"https\"}"],"Cf-Connecting-Ip":["x.x.x.x"],"Cdn-Loop":["cloudflare"],"Cookie":["_ga=GA1.2.678239338.1604131132; __cfduid=debfee6079a0d559f4c94455c399f01921608738744; cookieconsent_status=dismiss; PHPSESSID=7f388b4af221c6a4b4bffcf910602f61"],"Accept-Encoding":["gzip"],"Cf-Ipcountry":["GB"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],"Referer":["https://php.domain.com/"],"X-Forwarded-For":["x.x.x.x"],"Upgrade-Insecure-Requests":["1"],"Connection":["Keep-Alive"],"Cf-Ray":["608424bf0b660672-LHR"],"X-Forwarded-Proto":["https"],"Accept-Language":["en-GB,en;q=0.5"],"Cf-Request-Id":["0746754b6a00000672223f4000000001"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"","proto_mutual":true,"server_name":"php.domain.com"}},"common_log":"162.158.158.146 - - [27/Dec/2020:15:41:03 +0000] \"GET / HTTP/1.1\" 200 4835","duration":0.016583431,"size":4835,"status":200,"resp_headers":{"Content-Type":["text/html; charset=UTF-8"],"X-Powered-By":["PHP/7.4.13"],"Expires":["Thu, 19 Nov 1981 08:52:00 GMT"],"Cache-Control":["no-store, no-cache, must-revalidate"],"Pragma":["no-cache"],"Content-Encoding":["gzip"],"Vary":["Accept-Encoding"],"Server":["Caddy"]}}

Here’s the requested curl reponse with headers:

For the domain, no URI:

*   Trying 2606:4700:3031::ac43:83e8:443...
* TCP_NODELAY set
* Connected to php.domain.com (2606:4700:3031::ac43:83e8) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=CA; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Aug 18 00:00:00 2020 GMT
*  expire date: Aug 18 12:00:00 2021 GMT
*  subjectAltName: host "php.domain.com" matched cert's "*.domain.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x560edb507df0)
> GET / HTTP/2
> Host: php.domain.com
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200
< date: Sun, 27 Dec 2020 15:47:27 GMT
< content-type: text/html; charset=UTF-8
< set-cookie: __cfduid=d78bb1eb12a7090d24d4dc3b77bcae9141609084047; expires=Tue, 26-Jan-21 15:47:27 GMT; path=/; domain=.domain.com; HttpOnly; SameSite=Lax
< cache-control: no-store, no-cache, must-revalidate
< expires: Thu, 19 Nov 1981 08:52:00 GMT
< pragma: no-cache
< set-cookie: PHPSESSID=fe25b71ab098f4ccd2182c5ba050572a; path=/
< vary: Accept-Encoding
< x-powered-by: PHP/7.4.13
< cf-cache-status: DYNAMIC
< cf-request-id: 07467b279a0000bf23a836b000000001
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=k0r5CFqKpRKOGKcZQqNz5HFIrSGSEKex46doRjWNfPWAlInIUCrQcGbjvb7dJwaFn8dA3hFJ9BGC9MnbJlcwsbTtLSdja8fsIJPiGt7hL8JATgk%2F1KAfRk3z4Q%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"report_to":"cf-nel","max_age":604800}
< server: cloudflare
< cf-ray: 60842e1f5db8bf23-FRA
<

For the domain with /servers URI:

*   Trying 2606:4700:3035::681c:23f:443...
* TCP_NODELAY set
* Connected to php.domain.com(2606:4700:3035::681c:23f) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=CA; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Aug 18 00:00:00 2020 GMT
*  expire date: Aug 18 12:00:00 2021 GMT
*  subjectAltName: host "php.domain.com" matched cert's "*.domain.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x56003a962df0)
> GET /servers HTTP/2
> Host: php.domain.com
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 302
< date: Sun, 27 Dec 2020 15:51:28 GMT
< content-type: text/html; charset=UTF-8
< set-cookie: __cfduid=ddefb6922a55ec5c171b010bdcd1e77311609084288; expires=Tue, 26-Jan-21 15:51:28 GMT; path=/; domain=.domain.com; HttpOnly; SameSite=Lax
< cache-control: no-store, no-cache, must-revalidate
< expires: Thu, 19 Nov 1981 08:52:00 GMT
< location: https://php.domain.com
< pragma: no-cache
< set-cookie: PHPSESSID=4e9d41e2609d93ae66977c24ee98e2e6; path=/
< status: 302 Found
< vary: Accept-Encoding
< x-powered-by: PHP/7.4.13
< cf-cache-status: DYNAMIC
< cf-request-id: 07467ed75c00000601ab8d2000000001
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=UlUig6A%2Fbrb4Qx%2Fg6h%2BTpLfp0DoTBeeCiGi1un7orTXfc%2BEUy2Gwp6ONc2KrgWke2Lv8ofK8JJJkO9qysLYMFbpowAuTj5g11Qv%2FMOVPcTbPUReT55UD%2FQI5Yg%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"report_to":"cf-nel","max_age":604800}
< server: cloudflare
< cf-ray: 608434055b340601-FRA
<
* Connection #0 to host php.domain.com left intact

I’m afraid I’m in way over my confidence level.
Because of this, I might not be able to conclude anything from these files myself…

I have another copy of the site running on an apache server, so if you need any logs from there, to see an expected behaviour, I can fetch that as well.

As always, thanks for your help.
I’ll be sure to forward any information learned here, to the creator of the framework, and pursuade them to add it to their documentation, so hopefully others can enjoy the site on Caddy as well.

Which framework is it that you’re using? I could take a closer look at its documentation to see what I can figure out.

Premium URL Shortener.

They are very sparse on their documentation for installation though.

For your curl request, did you specify https:// or did you omit that? The 302 redirect there is to send you from HTTP to HTTPS.

Try by either specifying https:// or using curl -vL to follow redirects (L for Location, the redirect header name)

As for the config, I noticed a mistake I make, I think your app is expecting the path for admin to not include /admin in it. To solve that, just use handle_path instead of handle for that one. The difference is handle_path will strip the given path prefix before handing your rewrite, which should do what you expect, I think.