Docker with Caddy & PHP: Weird Redirect Issue

1. The problem I’m having:

New to Caddy and new-ish to web development in general.

I’m using Podman compose to set up Caddy and PHP. Both containers work and communicate with each other. However, no matter what URL I throw at it, it only shows the root index.php file. All subsequent sub-directories with a different index.php show the root index.php file instead. Non-existent paths also show the root index.php file. I would like to have a 404 for pages that do not exist and the correct index.php file to show for those that do.

2. Error messages and/or full log output:

No error message. The PHP logs list this each time I try to load a page:
127.0.0.1 - 18/Mar/2023:16:46:49 +0000 "GET /index.php" 200

3. Caddy version:

Caddy 2.6.4

4. How I installed and ran Caddy:

I used podman-compose
podman-compose --build

a. System environment:

Fedora Linux 37 (Server Edition)
Podman 4.4.2

b. Command:

caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

c. Service/unit/compose file:

My docker-compose.yaml file:

version: '3.9'

### SERVICES #################
services:
  ### PHP ####################
  php:
	build:
	  context: ./docker/php
	  args:
		- PHP_VERSION=8.2.3
		- PHP_VARIANT=fpm-alpine
		- PHP_EXTENSIONS="mysqli pdo pdo_mysql"
	networks:
	  - backend
	volumes:
	  - $PWD/websites/:/var/www/html
	environment:
	  - TZ=America/New_York
	container_name: php
	restart: always
  ### CADDY ##################
  caddy:
	build:
	  context: ./docker/caddy
	  args:
		- CADDY_VERSION=2.6.4
		- CADDY_MODULES="--with github.com/caddy-dns/cloudflare"
	depends_on:
	  - php
	networks:
	  - frontend
	  - backend
	ports:
	  - "80:80"
	  - "443:443"
	volumes:
	  - $PWD/caddy/Caddyfile:/etc/caddy/Caddyfile
	  - $PWD/websites:/var/www/html
	  - caddy-config:/config
	  - caddy-data:/data
	environment:
	  - TZ=America/New_York
	  - CF_API_TOKEN=MY_API_TOKEN
	container_name: caddy
	restart: always
networks:
  frontend:
	name: frontend
  backend:
	name: backend
volumes:
  caddy-config:
  caddy-data:

PHP (./docker/php/Dockerfile)

ARG PHP_VERSION
ARG PHP_VARIANT

FROM php:$PHP_VERSION-$PHP_VARIANT

ARG PHP_EXTENSIONS
RUN docker-php-ext-install $PHP_EXTENSIONS

Caddy (./docker/caddy/Dockerfile)

ARG CADDY_VERSION

FROM caddy:$CADDY_VERSION-builder AS builder

ARG CADDY_VERSION
ARG CADDY_MODULES

RUN xcaddy build $CADDY_MODULES
FROM caddy:$CADDY_VERSION

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

d. My complete Caddy config:

obscure.cc {
	tls cloudflare@example.com {
	  dns cloudflare {env.CF_API_TOKEN}
	}
	encode zstd gzip
	# Set this path to your site's directory.
	root * /var/www/html/

	# Serve a PHP site through php-fpm:
	php_fastcgi php:9000
 
	# Removed this per the suggestion below
	# rewrite * /index.php?{query}

	# Enable the static file server.
	file_server
}

The rewrite is probably the reason you only get /index.php everywhere, since that’s rewriting everything to that.

In addition, you’ve set your root to /var/www/html/, which is the also the path you’ve mounted your websites folder in the php container. However, you’ve mounted that folder to /usr/share/caddy in the caddy container, which seems unlikely to be correct.

Not being familiar with serving PHP leaves me unsure whether these are the only issues though.

Thanks for the reply! I have the website connected to both directories but that shouldn’t cause a problem because the $PWD/websites/ directory on the server just copies the files over to the /var/www/html directory for the PHP container and the /usr/share/caddy directory for the Caddy container. Probably redundant as Caddy is proxying the PHP directory but it shouldn’t impact the behavior I am seeing.

Removing the /usr/share/caddy volume from the docker-compose.yaml did not fix anything.

When I remove the rewrite command from the Caddyfile, it reverts to downloading the file rather than rendering it. I believe that is a PHP issue but I don’t understand how the rewrite makes it work and it doesn’t work without.

So at this point, I don’t know what changes need to be made to the Caddyfile or the docker-compose.yaml file to get the site to behave the way I would like it to. Since I can get PHP and Caddy to partially work with the rewrite, it makes me think that I have done something wrong with Caddy. But I don’t have enough experience with servers to understand what should be done to fix it.

You shouldn’t remove the volume, you absolutely should mount your PHP files in Caddy. But it needs to be in a consistent path in both the Caddy and PHP containers. Mount the files to /var/www/html in both containers for example.

After you’ve done that, make a request with curl -v and show what you see. And enable the debug global option, and show your logs in Caddy.

1 Like

Ok, I updated my docker-compose.yaml file so that both the Caddy and PHP services mount files to var/www/html. That fixed the one issue where existing files were redirecting to the root index.php file. Thanks!

I’m still having a problem with non-existent files showing up at root index.php rather than 404. I think this may be a default setting of php_fastcgi but I can’t figure out what to change to make the 404 show for files that do not exist.

Results of curl -v https://obscure.cc:

*   Trying 2606:4700:3031::6815:46c8:443...
* Connected to obscure.cc (2606:4700:3031::6815:46c8) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.obscure.cc
*  start date: Feb 22 18:15:13 2023 GMT
*  expire date: May 23 18:15:12 2023 GMT
*  subjectAltName: host "obscure.cc" matched cert's "obscure.cc"
*  issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1P5
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: obscure.cc]
* h2h3 [user-agent: curl/7.85.0]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x55f2101c4b50)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: obscure.cc
> user-agent: curl/7.85.0
> accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200
< date: Sun, 19 Mar 2023 05:07:22 GMT
< content-type: text/html; charset=UTF-8
< x-powered-by: PHP/8.2.3
< cf-cache-status: DYNAMIC
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=Z2VC4%2B4SZsr4GhZjoXiyHZnvHS2njq3k6kRDu9Jhn0QVblwp6ClOry%2BlQhNyu0UAIh7VFQQdF9FYyIvWdcQ0fASZ5xBTBN9w7j9T9ezhRhklgzacPiwegmdi37UmH582jUcVVok9jUdu"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=15552000; includeSubDomains; preload
< x-content-type-options: nosniff
< server: cloudflare
< cf-ray: 7aa33104186afad6-SJC
<
{The rendered HTML}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host obscure.cc left intact

Debug log:

2023/03/19 05:07:22.913 DEBUG   events  event   {"name": "tls_get_certificate", "id": "50a4b7c6-0e41-4ac3-a68c-4a8d79c1f64b", "origin": "tls", "data": {"client_hello":{"CipherSuites":[4865,4866,4867,49195,49196,49199,49200,49171,49192,156,157,47,53,10],"ServerName":"obscure.cc","SupportedCurves":[29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537,513],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771,770,769],"Conn":{}}}}

2023/03/19 05:07:22.914 DEBUG   tls.handshake   choosing certificate    {"identifier": "obscure.cc", "num_choices": 1}

2023/03/19 05:07:22.915 DEBUG   tls.handshake   default certificate selection results   {"identifier": "obscure.cc", "subjects": ["obscure.cc"], "managed": true, "issuer_key": "acme-v02.api.letsencrypt.org-directory", "hash": "f7c74044bc496ec447d21d6161c444c331ba3f49e155a3242a6173039f308525"}

2023/03/19 05:07:22.915 DEBUG   tls.handshake   matched certificate in cache    {"remote_ip": "172.69.22.224", "remote_port": "34146", "subjects": ["obscure.cc"], "managed": true, "expiration": "2023/06/17 03:57:59.000", "hash": "f7c74044bc496ec447d21d6161c444c331ba3f49e155a3242a6173039f308525"}

2023/03/19 05:07:22.920 DEBUG   http.handlers.rewrite   rewrote request {"request": {"remote_ip": "172.69.22.224", "remote_port": "34146", "proto": "HTTP/2.0", "method": "GET", "host": "obscure.cc", "uri": "/", "headers": {"Cdn-Loop": ["cloudflare"], "X-Forwarded-For": ["2600:3c01::f03c:93ff:feb8:f4df"], "Cf-Ray": ["7aa33104186afad6-SJC"], "X-Forwarded-Proto": ["https"], "Cf-Visitor": ["{\"scheme\":\"https\"}"], "Accept": ["*/*"], "Cf-Connecting-Ip": ["2600:3c01::f03c:93ff:feb8:f4df"], "Cf-Ipcountry": ["US"], "Accept-Encoding": ["gzip"], "User-Agent": ["curl/7.85.0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "obscure.cc"}}, "method": "GET", "uri": "/index.php"}

2023/03/19 05:07:22.921 DEBUG   http.handlers.reverse_proxy     selected upstream       {"dial": "php:9000", "total_upstreams": 1}

2023/03/19 05:07:22.922 DEBUG   http.reverse_proxy.transport.fastcgi    roundtrip       {"request": {"remote_ip": "172.69.22.224", "remote_port": "34146", "proto": "HTTP/2.0", "method": "GET", "host": "obscure.cc", "uri": "/index.php", "headers": {"User-Agent": ["curl/7.85.0"], "X-Forwarded-Host": ["obscure.cc"], "Cf-Visitor": ["{\"scheme\":\"https\"}"], "Accept": ["*/*"], "Cf-Ipcountry": ["US"], "Cf-Ray": ["7aa33104186afad6-SJC"], "X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip"], "Cf-Connecting-Ip": ["2600:3c01::f03c:93ff:feb8:f4df"], "Cdn-Loop": ["cloudflare"], "X-Forwarded-For": ["172.69.22.224"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "obscure.cc"}}, "env": {"REMOTE_HOST": "172.69.22.224", "HTTP_HOST": "obscure.cc", "HTTPS": "on", "SSL_PROTOCOL": "TLSv1.3", "GATEWAY_INTERFACE": "CGI/1.1", "REMOTE_ADDR": "172.69.22.224", "DOCUMENT_URI": "/index.php", "SERVER_PORT": "443", "SERVER_PROTOCOL": "HTTP/2.0", "DOCUMENT_ROOT": "/var/www/html", "HTTP_X_FORWARDED_PROTO": "https", "QUERY_STRING": "", "HTTP_CDN_LOOP": "cloudflare", "REQUEST_SCHEME": "https", "SERVER_NAME": "obscure.cc", "SERVER_SOFTWARE": "Caddy/v2.6.4", "SCRIPT_NAME": "/index.php", "HTTP_CF_RAY": "7aa33104186afad6-SJC", "CONTENT_TYPE": "", "REMOTE_PORT": "34146", "AUTH_TYPE": "", "REMOTE_IDENT": "", "REQUEST_METHOD": "GET", "HTTP_CF_CONNECTING_IP": "2600:3c01::f03c:93ff:feb8:f4df", "HTTP_ACCEPT": "*/*", "HTTP_CF_IPCOUNTRY": "US", "CONTENT_LENGTH": "", "REMOTE_USER": "", "HTTP_X_FORWARDED_FOR": "172.69.22.224", "HTTP_CF_VISITOR": "{\"scheme\":\"https\"}", "HTTP_USER_AGENT": "curl/7.85.0", "PATH_INFO": "", "SSL_CIPHER": "TLS_AES_128_GCM_SHA256", "HTTP_ACCEPT_ENCODING": "gzip", "HTTP_X_FORWARDED_HOST": "obscure.cc", "REQUEST_URI": "/", "SCRIPT_FILENAME": "/var/www/html/index.php"}, "dial": "php:9000", "env": {"PATH_INFO": "", "SSL_CIPHER": "TLS_AES_128_GCM_SHA256", "HTTP_X_FORWARDED_FOR": "172.69.22.224", "HTTP_CF_VISITOR": "{\"scheme\":\"https\"}", "HTTP_USER_AGENT": "curl/7.85.0", "R
RI": "/", "SCRIPT_FILENAME": "/var/www/html/index.php", "HTTP_ACCEPT_ENCODING": "gzip", "HTTP_X_FORWARDED_HOST": "obscure.cc", "GATEWAY_INTERFACE": "CGI/1.1", "REMOTE_ADDR": "172.69.22.224", "REMOTE_HOST": "172.69.22.224", "HTTP_HOST": "obscure.cc", "HTTPS": "on", "SSL_PROTOCOL": "TLSv1.3", "SERVER_PROTOCOL": "HTTP/2.0", "DOCUMENT_ROOT": "/var/www/html", "DOCUMENT_URI": "/index.php", "SERVER_PORT": "443", "QUERY_STRING": "", "HTTP_CDN_LOOP": "cloudflare", "HTTP_X_FORWARDED_PROTO": "https", "CONTENT_TYPE": "", "REMOTE_PORT": "34146", "REQUEST_SCHEME": "https", "SERVER_NAME": "obscure.cc", "SERVER_SOFTWARE": "Caddy/v2.6.4", "SCRIPT_NAME": "/index.php", "HTTP_CF_RAY": "7aa33104186afad6-SJC", "AUTH_TYPE": "", "REMOTE_IDENT": "", "CONTENT_LENGTH": "", "REMOTE_USER": "", "REQUEST_METHOD": "GET", "HTTP_CF_CONNECTING_IP": "2600:3c01::f03c:93ff:feb8:f4df", "HTTP_ACCEPT": "*/*", "HTTP_CF_IPCOUNTRY": "US"}, "request": {"remote_ip": "172.69.22.224", "remote_port": "34146", "proto": "HTTP/2.0", "method": "GET", "host": "obscure.cc", "uri": "/index.php", "headers": {"Cf-Connecting-Ip": ["2600:3c01::f03c:93ff:feb8:f4df"], "Cdn-Loop": ["cloudflare"], "X-Forwarded-For": ["172.69.22.224"], "Cf-Ray": ["7aa33104186afad6-SJC"], "X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip"], "Cf-Visitor": ["{\"scheme\":\"https\"}"], "Accept": ["*/*"], "Cf-Ipcountry": ["US"], "User-Agent": ["curl/7.85.0"], "X-Forwarded-Host": ["obscure.cc"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "obscure.cc"}}}

2023/03/19 05:07:22.924 DEBUG   http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "php:9000", "duration": 0.002511522, "request": {"remote_ip": "172.69.22.224", "remote_port": "34146", "proto": "HTTP/2.0", "method": "GET", "host": "obscure.cc", "uri": "/index.php", "headers": {"Cf-Visitor": ["{\"scheme\":\"https\"}"], "Accept": ["*/*"], "Cf-Ipcountry": ["US"], "User-Agent": ["curl/7.85.0"], "X-Forwarded-Host": ["obscure.cc"], "Cf-Connecting-Ip": ["2600:3c01::f03c:93ff:feb8:f4df"], "Cdn-Loop": ["cloudflare"], "X-Forwarded-For": ["172.69.22.224"], "Cf-Ray": ["7aa33104186afad6-SJC"], "X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "obscure.cc"}}, "headers": {"X-Powered-By": ["PHP/8.2.3"], "Content-Type": ["text/html; charset=UTF-8"]}, "status": 200}

Well your logs and curl -v look correct. What’s the problem now? Did you remove the rewrite? You should if you didn’t.

I removed the rewrite so now files that exist are rendering correctly. The only remaining problem is non-existent files are not showing a 404. Rather, the root index.php file is shown instead. I suspect this is a setting within php_fastcgi but I don’t know what to tweak in the long version to make the 404 show (or if that is even the problem).

This is normal behaviour. In modern PHP apps, the index.php is the routing entrypoint. It’s the index.php file’s job to respond with a 404 if the REQUEST_URI does not map to a file it wants to serve.

1 Like

Ok, I’m not running any PHP frameworks or apps. Just a few little hobby projects that happen to use PHP to do some small dynamic things. Your first response fixed the bulk of my issue. I will figure out how to get Caddy to issue a 404 instead or figure out how modern PHP apps work and PHP do that. Thanks so much for your help!

The way you could fix it instead is like this:

php_fastcgi php:9000 {
	try_files {path} {path}/index.php =404
}

This would cause requests to instead 404 if it doesn’t map to an actual file. So that means PHP will only handle requests whose path is exactly a PHP file.

See the last example here:

When I add this line, I get a 404 via cURL but my browser attempts to download the non-existent file. I suspect this is a PHP issue and will consider this closed because Caddy is doing what it is supposed to be doing. Thanks again for all your help!

Ok, the following lines did the trick:

php_fastcgi php:9000 {
    try_files {path} {path}/index.php =404
 }

handle_errors {
    rewrite * /error.html
    templates
    file_server
}

I just needed to tell Caddy where my error files were located :person_facepalming:. Thanks again for your help.

Final working Caddyfile:

obscure.cc {
    # Setup Cloudflare so certificates can be auto-issued and renewed
    tls cloudflare@example.com {
      dns cloudflare {env.CF_API_TOKEN}
    }

    # Compress files
    encode zstd gzip

    # Set this path to your site's directory.
    root * /var/www/html/

    # Serve a PHP site through php-fpm:
    php_fastcgi php:9000 {
      try_files {path} {path}/index.php =404
    }

    handle_errors {
      rewrite * /error.html
      templates
      file_server
    }

    # Enable the static file server.
    file_server
}

Then just create your custom error code page at your root directory and name it error.html. For example:

…
<h1>{{placeholder "http.error.status_code"}}</h1>

<p>{{placeholder "http.error.status_text"}}</p>
…
1 Like

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