Caddy with FrankenPHP docker setup

1. The problem I’m having:

Hi there, I’m new to web development and need some help with setting up a server but first I need to get it running locally.

I’m trying to get one caddy container as reverse proxy that serves multiple PHP applications in their own seperated containers using docker-compose.yml

This is my idea before trying different and simple alternative.

  • Caddy Server - Image: caddy:2.7.6-alpine
  • PHP RSVP App - Image: dunglas/frankenphp ← This is also based off caddy image
  • and many more PHP app that will be added later.

I can curl PHP RSVP App from Caddy Server without any problem. But when I tried to access using my browser it returns an empty page.

2. Error messages and/or full log output:

curl response from Caddy Server - SUCCESS

/srv # curl -v rsvp
* Host rsvp:80 was resolved.
* IPv6: (none)
* IPv4: 172.29.0.3
*   Trying 172.29.0.3:80...
* Connected to rsvp (172.29.0.3) port 80
> GET / HTTP/1.1
> Host: rsvp
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Server: Caddy
< X-Powered-By: PHP/8.3.3
< Date: Fri, 08 Mar 2024 05:53:52 GMT
< Content-Length: 15
< 
Hello from PHP
* Connection #0 to host rsvp left intact

curl response from my Mac - FAIL

note: I have set /etc/hosts to point rsvp.local to 127.0.01

curl -v rsvp.local
*   Trying 127.0.0.1:80...
* Connected to rsvp.local (127.0.0.1) port 80
> GET / HTTP/1.1
> Host: rsvp.local
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Caddy
< Date: Fri, 08 Mar 2024 05:54:56 GMT
< Content-Length: 0
<
* Connection #0 to host rsvp.local left intact

3. Caddy version:

  • Revere proxy - caddy:2.7.6-alpine
  • PHP App - dunglas/frankenphp

4. How I installed and ran Caddy:

a. System environment:

Running on 2017 Intel Macbook Pro - Docker v4.28.0

My mac host’s etc/hosts has been setup to point rsvp.local to 127.0.0.1

b. Command:

docker compose up -d --build

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:2.7.6-alpine
    restart: always
    ports:
      - "80:80"
      # - "443:443"
      # - "443:443/udp"
    volumes:
      - ./caddy/data:/data
      - ./caddy/config:/config
      - ./caddy/caddyfile/:/etc/caddy/
    networks:
      - backend
  rsvp:
    container_name: rsvp
    image: dunglas/frankenphp
    restart: always
    ports:
      - "81:80"
      # - "444:443"
      # - "444:443/udp"
    volumes:
      - ./app:/app
    environment:
      - SERVER_NAME=rsvp:80
    networks:
      - backend

networks:
    backend:
        driver: bridge

d. My complete Caddy config:

Caddyfile

rsvp.local:80 {
        reverse_proxy rsvp
}

The ./app/public/index.php contains

<?php

echo "Hello from PHP";

echo PHP_EOL;

5. Links to relevant resources:

If you’re using FrankenPHP, you don’t need to put another Caddy in front of it. Just make requests directly to FrankenPHP. You can configure your Caddyfile in the FrankenPHP container. See https://frankenphp.dev/docs/config/

Hi @francislavoie, thank you for your response.

Yes, I previously tried your method above, and it works perfectly.

The reason I added an extra reverse proxy at the front is to containerize and isolate each PHP application. This way, each application will have its own environment, preventing them from accessing other application code if anything goes wrong.

Since my initial setup didn’t work out, I’ve decided to follow your recommendation.

Could you suggest any best practices or resources for isolating applications, such as setting up permissions, etc., based on your recommendation?

Fair enough.

I don’t see why it wouldn’t work. Are you sure Caddy is running with the config you expect? Add the debug global option, add the log directive in your site. What’s in your logs?

I have added the global debug and log in the application but not sure if this is what you mean.

{
	debug
}

rsvp.local:80 {
	reverse_proxy rsvp
	log default {
		output stdout
		format json
		level DEBUG
	}
}

The logs look like this

caddy-server

2024-03-09 14:20:31 {"level":"debug","ts":1709947231.7898073,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"rsvp:80","total_upstreams":1}
2024-03-09 14:20:31 {"level":"debug","ts":1709947231.790681,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"rsvp:80","duration":0.000790294,"request":{"remote_ip":"192.168.65.1","remote_port":"36100","client_ip":"192.168.65.1","proto":"HTTP/1.1","method":"GET","host":"rsvp.local","uri":"/","headers":{"User-Agent":["curl/8.4.0"],"Accept":["*/*"],"X-Forwarded-For":["192.168.65.1"],"X-Forwarded-Proto":["http"],"X-Forwarded-Host":["rsvp.local"]}},"headers":{"Server":["Caddy"],"Date":["Sat, 09 Mar 2024 01:20:31 GMT"],"Content-Length":["0"]},"status":200}
2024-03-09 14:20:31 {"level":"info","ts":1709947231.790782,"logger":"http.log.access.default","msg":"handled request","request":{"remote_ip":"192.168.65.1","remote_port":"36100","client_ip":"192.168.65.1","proto":"HTTP/1.1","method":"GET","host":"rsvp.local","uri":"/","headers":{"User-Agent":["curl/8.4.0"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.000996463,"size":0,"status":200,"resp_headers":{"Server":["Caddy","Caddy"],"Date":["Sat, 09 Mar 2024 01:20:31 GMT"],"Content-Length":["0"]}}

The rsvp PHP app logs

2024-03-09 14:12:56 {"level":"debug","ts":1709946776.7378323,"logger":"admin.api","msg":"received request","method":"GET","host":"localhost:2019","uri":"/metrics","remote_ip":"127.0.0.1","remote_port":"35130","headers":{"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2024-03-09 14:13:26 {"level":"debug","ts":1709946806.7950742,"logger":"admin.api","msg":"received request","method":"GET","host":"localhost:2019","uri":"/metrics","remote_ip":"127.0.0.1","remote_port":"49008","headers":{"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2024-03-09 14:13:56 {"level":"debug","ts":1709946836.8524058,"logger":"admin.api","msg":"received request","method":"GET","host":"localhost:2019","uri":"/metrics","remote_ip":"127.0.0.1","remote_port":"45114","headers":{"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}

Edit - rsvp PHP app

2024-03-09 14:26:23 {"level":"info","ts":1709947583.8256292,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"172.30.0.3","remote_port":"58502","client_ip":"172.30.0.3","proto":"HTTP/1.1","method":"GET","host":"rsvp.local","uri":"/","headers":{"X-Forwarded-Host":["rsvp.local"],"X-Forwarded-Proto":["http"],"Accept-Encoding":["gzip"],"User-Agent":["curl/8.4.0"],"Accept":["*/*"],"X-Forwarded-For":["192.168.65.1"]}},"bytes_read":0,"user_id":"","duration":0.00016692,"size":0,"status":0,"resp_headers":{"Server":["Caddy"]}}

Ah, I see what’s going on. The request was successfully proxied, but the problem is your upstream is only serving the rsvp hostname. Caddy passes through the original Host as-is, so the FrankenPHP upstream sees rsvp.local which it wasn’t configured to handle.

The easiest solution is to set SERVER_NAME=:80 on your FrankenPHP upstream, i.e. no specific hostname. That way, the hostname doesn’t matter.

Thanks @francislavoie. You are a legend!

It worked!

❯ curl -v rsvp.local
*   Trying 127.0.0.1:80...
* Connected to rsvp.local (127.0.0.1) port 80
> GET / HTTP/1.1
> Host: rsvp.local
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 15
< Content-Type: text/html; charset=UTF-8
< Date: Sat, 09 Mar 2024 01:46:44 GMT
< Server: Caddy
< Server: Caddy
< X-Powered-By: PHP/8.3.3
<
Hello from PHP
* Connection #0 to host rsvp.local left intact
1 Like

You might want to add trusted_proxies to your FrankenPHP as well (to preserve the original IP address via the X-Forwarded-For header, see Common Caddyfile Patterns — Caddy Documentation), and remove the port 81 mapping (so it’s only accessible through Caddy).

Thank you. That really helps.

1 Like

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