Caddy + PHP-FPM is not accessible from host

1. The problem I’m having:

I want to create a single container with Caddy and PHP-FPM. To do so I’ve created a Docker image based on ‘php:8.3.2-fpm-alpine’ with Caddy installed as dependency. To keep PHP-FPM up and running I rebuilt Caddy in order to use Module supervisor instead of using SupervisorD.

With this approach the domain https://demo.localhost is signed and properly accessed from inside the container but cannot be accessed from the host.

2. Error messages and/or full log output:

[+] Running 2/1
 ✔ Network microservice-caddy-php-v2_default  Created                                                                                                                                     0.1s 
 ✔ Container app                              Created                                                                                                                                     0.0s 
Attaching to app
app  | {"level":"info","ts":1707931901.4808557,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
app  | {"level":"info","ts":1707931901.482843,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
app  | {"level":"info","ts":1707931901.4831343,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00039fc00"}
app  | {"level":"info","ts":1707931901.4859724,"logger":"http.auto_https","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
app  | {"level":"info","ts":1707931901.4859855,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
app  | {"level":"warn","ts":1707931901.4859886,"logger":"http.auto_https","msg":"server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server","server_name":"srv1","http_port":80}
app  | {"level":"warn","ts":1707931901.5037982,"logger":"pki.ca.local","msg":"installing root certificate (you might be prompted for password)","path":"storage:pki/authorities/local/root.crt"}
app  | {"level":"info","ts":1707931901.505052,"msg":"define JAVA_HOME environment variable to use the Java trust"}
app  | {"level":"info","ts":1707931901.5050712,"msg":"not NSS security databases found"}
app  | {"level":"info","ts":1707931901.529142,"msg":"certificate installed properly in linux trusts"}
app  | {"level":"info","ts":1707931901.5295074,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
app  | {"level":"info","ts":1707931901.529732,"msg":"failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details."}
app  | {"level":"info","ts":1707931901.5305398,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
app  | {"level":"info","ts":1707931901.5305696,"logger":"http.log","msg":"server running","name":"srv1","protocols":["h1","h2","h3"]}
app  | {"level":"info","ts":1707931901.5305738,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["demo.localhost"]}
app  | {"level":"info","ts":1707931901.530703,"logger":"tls","msg":"cleaning storage unit","storage":"FileStorage:/root/.local/share/caddy"}
app  | {"level":"info","ts":1707931901.53072,"msg":"autosaved config (load with --resume flag)","file":"/root/.config/caddy/autosave.json"}
app  | {"level":"info","ts":1707931901.530726,"msg":"serving initial configuration"}
app  | {"level":"info","ts":1707931901.5308032,"logger":"tls.obtain","msg":"acquiring lock","identifier":"demo.localhost"}
app  | {"level":"info","ts":1707931901.530809,"logger":"tls","msg":"finished cleaning storage units"}
app  | {"level":"info","ts":1707931901.530929,"logger":"supervisor","msg":"process started","command":["php-fpm","--nodaemonize"],"replica":0,"pid":23}
app  | {"level":"info","ts":1707931901.531587,"logger":"tls.obtain","msg":"lock acquired","identifier":"demo.localhost"}
app  | {"level":"info","ts":1707931901.5316172,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"demo.localhost"}
app  | {"level":"info","ts":1707931901.5326047,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"demo.localhost"}
app  | {"level":"info","ts":1707931901.5326388,"logger":"tls.obtain","msg":"releasing lock","identifier":"demo.localhost"}
app  | {"level":"warn","ts":1707931901.5328276,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [demo.localhost]: no OCSP server specified in certificate","identifiers":["demo.localhost"]}
app  | [14-Feb-2024 17:31:41] NOTICE: fpm is running, pid 23
app  | [14-Feb-2024 17:31:41] NOTICE: ready to handle connections

Accessing the domain from Inside the container

$ docker exec -it app curl https://demo.localhost
OK

The index.php file just print out an ‘OK’ so this output is the expected response.

Accessing the domain from the host

$ curl https://demo.localhost 
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

3. Caddy version:

$ docker exec -it app caddy --version            
v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

Dockerfile

# syntax=docker/dockerfile:1

# STAGE #########################################################

FROM caddy:2.7.6-builder-alpine as caddy-builder

RUN xcaddy build \
    --with github.com/baldinof/caddy-supervisor


# STAGE #########################################################

FROM php:8.3.2-fpm-alpine as base

RUN apk update && apk add --no-cache \
        caddy \
        fcgi \
        nss-tools

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

WORKDIR /code

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]


# STAGE #########################################################

FROM php:8.3.2-fpm-alpine as build-development-extensions

RUN curl -sSL https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions -o - | sh -s \
        pcov \
        uopz

# STAGE #########################################################

FROM base as build-development

ARG HOST_USER_ID=1000
ARG HOST_USER_NAME=host-user-name

ARG HOST_GROUP_ID=1000
ARG HOST_GROUP_NAME=host-group-name

ENV ENV=DEVELOPMENT

COPY --from=composer /usr/bin/composer /usr/bin/composer

COPY --from=build-development-extensions /usr/local/lib/php/extensions/*/* /usr/local/lib/php/extensions/no-debug-non-zts-20230831
COPY --from=build-development-extensions /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d/

RUN apk update && apk add --no-cache \
        bash \
        util-linux

RUN addgroup --gid ${HOST_GROUP_ID} ${HOST_GROUP_NAME} \
    && adduser --shell /bin/bash --uid ${HOST_USER_ID} --ingroup ${HOST_GROUP_NAME} --ingroup www-data --disabled-password --gecos '' ${HOST_USER_NAME}

COPY ./build/www.conf /usr/local/etc/php-fpm.d/www.conf
RUN sed -i -r "s/USER-NAME/${HOST_USER_NAME}/g" /usr/local/etc/php-fpm.d/www.conf \
    && sed -i -r "s/GROUP-NAME/${HOST_GROUP_NAME}/g" /usr/local/etc/php-fpm.d/www.conf

a. System environment:

  • Laptop: Asus ZenBook 13
  • CPU: 11th Gen Intel(R) Core™ i7-1165G7 @ 2.80GHz
  • RAM: 16Gb
  • OS: Linux ZenBook-UX325EA 6.6.0-14-generic #14-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 30 10:27:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
  • Docker version: 24.0.7, build 24.0.7-0ubuntu1

b. Command:

Build the image

$ docker-compose build --build-arg="HOST_USER_NAME=$(id --user --name)"  --build-arg="HOST_GROUP_NAME=$(id --user --name)" 

Start the service

$ docker-compose up --remove-orphans 

c. Service/unit/compose file:

docker-compose.yml

version: '3.9'

volumes:
  caddy_data:
  caddy_config:

services:
    app:
        container_name: app
        build:
            context: .
            dockerfile: Dockerfile
            target: build-development
        restart: unless-stopped
        volumes:
            - ./build/Caddyfile:/etc/caddy/Caddyfile
            - ./src:/code
            - caddy_data:/data
            - caddy_config:/config
        ports:
            - 80:80
            - 443:443
            - 443:443/udp
        healthcheck:
            test: ["CMD", "wget", "--tries=1", "--spider", "https://demo.localhost/metrics"]
            interval: 10s
            timeout: 1s
            retries: 3

d. My complete Caddy config:

{
	supervisor {
		php-fpm --nodaemonize {
			env DEBUG false
			redirect_stdout stdout
			redirect_stderr stdout
			restart_policy on_failure
		}
	}
}

:80 {
	respond "Hi!"
}

demo.localhost {
	metrics /metrics
	tls internal
	encode zstd gzip
	root * /code/public
	php_fastcgi :9000
	file_server
}

e. The PHP-FPM config file

[www]
user=USER-NAME
group=GROUP-NAME
listen=:9000
ping.path=/status/ping
pm=dynamic
pm.max_children=300
pm.max_requests=10240
pm.max_spare_servers=35
pm.min_spare_servers=5
pm.process_idle_timeout=0s;
pm.start_servers=20
pm.status_path=/status/php-fpm
request_slowlog_timeout=5
request_terminate_timeout=1200
rlimit_files=65535
slowlog=/var/log/$pool.log.slow

5. Links to relevant resources:

If I check the path where the certificates are supposed to be generated, that path is empty:

$ docker exec -it app ls -la /data/              
total 8
drwxr-xr-x    2 root     root          4096 Feb 14 16:07 .
drwxr-xr-x    1 root     root          4096 Feb 14 18:12 ..

Your host machine doesn’t trust Caddy’s certificates.

See the docs, which explain how to set that up:

That said, I recommend you use https://frankenphp.dev/ instead of PHP-FPM, you’ll have much better performance than PHP-FPM can offer. (It’s a distribution of Caddy that ships with PHP compiled in, so PHP is invoked directly from Caddy)

Hi @francislavoie,

Thanks for replying my topic. I tried to extract the Caddy’s certificate from the container but those files are missing

$ docker exec -it app ls -la /data/                                 
total 8
drwxr-xr-x    2 root     root          4096 Feb 14 16:07 .
drwxr-xr-x    1 root     root          4096 Feb 15 08:43 ..

So it’s impossible to make my host trust Caddy at this point

Ah, that’s because you didn’t set the env vars necessary in your Dockerfile such that Caddy would write its data to /data.

See caddy-docker/2.7/alpine/Dockerfile at master · caddyserver/caddy-docker · GitHub

Hi @francislavoie

Now it’s working as expected!

Thank you!!

1 Like

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