frankenphp
frankenphp copied to clipboard
Proxying FrankenPHP to Caddy (technically Caddy to Caddy) not working
What happened?
I think I tried everything, included the web help and forums. After that I am not sure the problem is here or in my code.
I have a project, based on API Platform, w/ the newest versions of deps on frontend and backend . (That can possible right now.)
The backend deps
$ composer outdated
Color legend:
- patch or minor release available - update recommended
- major release available - update possible
Direct dependencies required in composer.json:
doctrine/orm 2.19.5 3.1.3 Object-Relational-Mapper for PHP
Transitive dependencies not required in composer.json:
doctrine/dbal 3.8.4 4.0.2 Powerful PHP database abstraction layer (DBAL) with many features for database schem...
paragonie/constant_time_encoding 2.7.0 3.0.0 Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)
phenx/php-font-lib 0.5.6 1.0.0 A library to read, parse, export and make subsets of different types of font files.
phenx/php-svg-lib 0.5.4 1.0.0 A library to read, parse and export to PDF SVG files.
Frontend deps
$ pnpm outdated
┌──────────────────────┬─────────┬────────┐
│ Package │ Current │ Latest │
├──────────────────────┼─────────┼────────┤
│ eslint (dev) │ 8.57.0 │ 9.2.0 │
├──────────────────────┼─────────┼────────┤
│ postcss-loader (dev) │ 7.3.4 │ 8.1.1 │
└──────
I would like to use the FrankenPHP in worker mode as a standalone binary. So, I build the frontend to production
$ pnpm build
and I create the binary file. For that, I have a Makefile entry point what is based on the FrankenPHP's docs.
Binary logs
$ make build-binary
docker build -t static-app -f static-build.Dockerfile .
[+] Building 45.9s (10/10) FINISHED docker:default
=> [internal] load build definition from static-build.Dockerfile 0.0s
=> => transferring dockerfile: 342B 0.0s
=> [internal] load metadata for docker.io/dunglas/frankenphp:static-builder 0.5s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/dunglas/frankenphp:static-builder@sha256:8b22271a16e7958f580af7e922bbcd211d491eefcf14f777c4b349ffbcf6be98 0.0s
=> [internal] load build context 2.1s
=> => transferring context: 22.53MB 2.0s
=> CACHED [2/5] WORKDIR /go/src/app/dist/app 0.0s
=> CACHED [3/5] COPY . . 0.0s
=> CACHED [4/5] WORKDIR /go/src/app/ 0.0s
=> [5/5] RUN EMBED=dist/app/ NO_COMPRESS=1 PHP_EXTENSIONS=ctype,iconv,simplexml,tokenizer,session,pdo_mysql,opcache,apcu,curl,dom,openssl ./build-static.sh 20.6s
=> exporting to image 22.5s
=> => exporting layers 22.4s
=> => writing image sha256:ca6a61e1e8932cab08b47d8159d8f9bf6e3683e3c70b4e5d65f556f63b951bef 0.0s
=> => naming to docker.io/library/static-app 0.0s
docker cp b2fe9a88d0fe65f8e7912430661a9d6dadc931992838756c3264ced32d6d6a8c:/go/src/app/dist/frankenphp-linux-x86_64 yc-api-binary ; docker rm static-app-tmp
Successfully copied 1.07GB to /var/www/splendid/yc-api/yc-api-binary
static-app-tmp
I do not use compressing (because it is so slow) and I do not remove the unnecessary dirs and files yet. But for production build, I will use these recommendations on the production server.
Until this, everything works as expected, the problem is to reach the site. Even if I do the recommended things I mentioned above.
The 1st approach
I try to use the binary alone without another Caddy. For that, w/ default settings I have to run the binary w/ sudo beacuse the 443 port cannot binded. (Permission error)
$ sudo ./yc-api-binary php-server
The server starts as expected
2024/05/12 10:02:37.094 INFO using provided configuration {"config_file": "/tmp/frankenphp_fd3f9cdbef5d517ff0c6fb2814407066 app.tar/Caddyfile", "config_adapter": ""}
2024/05/12 10:02:37.094 WARN Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies {"adapter": "caddyfile", "file": "/tmp/frankenphp_fd3f9cdbef5d517ff0c6fb2814407066 app.tar/Caddyfile", "line": 2}
2024/05/12 10:02:37.094 WARN admin admin endpoint disabled
2024/05/12 10:02:37.094 INFO http.auto_https 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}
2024/05/12 10:02:37.094 INFO http.auto_https enabling automatic HTTP->HTTPS redirects {"server_name": "srv0"}
2024/05/12 10:02:37.094 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc00059ca80"}
2024/05/12 10:02:37.095 INFO FrankenPHP started 🐘 {"php_version": "8.3.7"}
2024/05/12 10:02:37.095 INFO embedded PHP app 📦 {"path": "/tmp/frankenphp_fd3f9cdbef5d517ff0c6fb2814407066 app.tar"}
2024/05/12 10:02:37.105 INFO tls cleaning storage unit {"storage": "FileStorage:/root/.local/share/caddy"}
2024/05/12 10:02:37.105 INFO tls certificate expired beyond grace period; cleaning up {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/localhost/localhost.crt", "expired_for": 1341850.105486614, "grace_period": 1209600}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/localhost/localhost.crt"}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/localhost/localhost.key"}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/localhost/localhost.json"}
2024/05/12 10:02:37.105 INFO tls deleting site folder because key is empty {"storage": "FileStorage:/root/.local/share/caddy", "site_key": "certificates/local/localhost"}
2024/05/12 10:02:37.105 INFO tls certificate expired beyond grace period; cleaning up {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/yc-api.local/yc-api.local.crt", "expired_for": 2035682.105638877, "grace_period": 1209600}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/yc-api.local/yc-api.local.crt"}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/yc-api.local/yc-api.local.key"}
2024/05/12 10:02:37.105 INFO tls deleting asset because resource expired {"storage": "FileStorage:/root/.local/share/caddy", "asset_key": "certificates/local/yc-api.local/yc-api.local.json"}
2024/05/12 10:02:37.105 INFO tls deleting site folder because key is empty {"storage": "FileStorage:/root/.local/share/caddy", "site_key": "certificates/local/yc-api.local"}
2024/05/12 10:02:37.105 INFO tls finished cleaning storage units
2024/05/12 10:02:37.106 INFO pki.ca.local root certificate is already trusted by system {"path": "storage:pki/authorities/local/root.crt"}
2024/05/12 10:02:37.106 INFO pki intermediate expires soon; renewing {"ca": "local", "time_remaining": -780251.106149375}
2024/05/12 10:02:37.106 INFO pki renewed intermediate {"ca": "local", "new_expiration": "2024/05/19 10:02:37.000"}
2024/05/12 10:02:37.106 INFO http enabling HTTP/3 listener {"addr": ":443"}
2024/05/12 10:02:37.106 INFO http.log server running {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2024/05/12 10:02:37.106 INFO http.log server running {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2024/05/12 10:02:37.106 INFO http enabling automatic TLS certificate management {"domains": ["localhost"]}
2024/05/12 10:02:37.106 INFO tls.obtain acquiring lock {"identifier": "localhost"}
2024/05/12 10:02:37.107 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"}
2024/05/12 10:02:37.115 INFO tls.obtain lock acquired {"identifier": "localhost"}
2024/05/12 10:02:37.115 INFO tls.obtain obtaining certificate {"identifier": "localhost"}
2024/05/12 10:02:37.116 INFO tls.obtain certificate obtained successfully {"identifier": "localhost"}
2024/05/12 10:02:37.116 INFO tls.obtain releasing lock {"identifier": "localhost"}
2024/05/12 10:02:37.116 WARN tls stapling OCSP {"error": "no OCSP stapling for [localhost]: no OCSP server specified in certificate", "identifiers": ["localhost"]}
After this, I can reach the site completely. I can log in the site but after this, the first request crashes the server w/ segfault
[1] 2376515 segmentation fault sudo ./yc-api-binary php-server
The 2nd approach
This is similar what the production environment would be looks like.
I have a Caddy on production environment. On that, I have a Caddy config what proxies the requests to the FrankenPHP's binary. The Caddy config for the prod server:
yc-franken.local {
reverse_proxy http://localhost:9081
log {
output file /var/log/caddy/yc-franken.access.log {
roll_size 3MiB
roll_keep 5
roll_keep_for 48h
}
format console
}
}
and this is the Caddyfile in the project's root:
{
http_port 9081
https_port 9043
admin off
frankenphp
order php_server before file_server
}
localhost {
root * ./public
encode zstd br gzip
php_server
}
If I use the config w/o the http_port and https_port global options, the server obviously not starting, because the port collision. And I do not need the admin, so I switched it off.
With this settings the server starts correctly. But I cannot reach any site because too much redirections
So, I use another global option, the auto_https w/ off
{
http_port 9081
https_port 9043
admin off
auto_https off
...
}
...
This is not working too. The problem is that, somehow the script wanted to download the /api/docs.jsonld entry point on http, w/o ssl.
Meanwhile I use the newest @api-platform/admin (3.4.6) and I have this data-provider on HydraAdmin
assets/js/core/data-provider.js
import { fetchHydra, hydraDataProvider } from '@api-platform/admin';
import { parseHydraDocumentation } from '@api-platform/api-doc-parser';
import { dataProviderExtension, ENTRYPOINT } from '@yc/core';
const getHeaders = () => {
const headers = {};
const token = window.localStorage.getItem('auth');
if (token) headers.Authorization = `Bearer ${token}`;
const locale = window.localStorage.getItem('locale');
if (locale) headers['X-Locale'] = locale;
return headers;
};
export const dataProvider = setRedirectToLogin => ({
...hydraDataProvider(
{
entrypoint: ENTRYPOINT,
docEntrypoint: `${ENTRYPOINT}/docs.jsonld`,
httpClient: (url, options = {}) => {
return fetchHydra(url, {
...options,
headers: getHeaders,
});
},
apiDocumentationParser: async () => {
try {
setRedirectToLogin(false);
return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders });
} catch (result) {
const { api, response, status } = result;
if (401 !== status || !response) {
throw result;
}
// Prevent infinite loop if the token is expired
localStorage.removeItem('token');
setRedirectToLogin(true);
return {
api,
response,
status,
};
}
},
},
),
...(dataProviderExtension()),
});
In this file the ENTRY_POINT is /api and every entry point works I have defined, except the docs.jsonld. Even if I forced the entry point root to the domain name, i.e.: https://example.com/api..
Build Type
Docker (Alpine)
Worker Mode
Yes
Operating System
GNU/Linux
CPU Architecture
x86_64
PHP configuration
I think, it is irrelevant to the issue.
Relevant log output
These are in the description.
For the too many redirects issue, take a look at the Location header to try and diagnose where the redirect comes from. I think you need to discover if it is coming from one of the Caddies or your php code.
For the segfault, a stack trace would be useful.
I'm on my mobile atm, so I will have to look up the link and edit this. Or search the other issues for how to do this.
For the segfault, a stack trace would be useful.
How can I give you a dev stack trace from this? I could not find relevant issue.
The console output might be irrelevant, the last couple of rows:
{"message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","context":{"exception":{"class":"ErrorException","message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","code":0,"file":"/tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83"}},"level":200,"level_name":"INFO","channel":"deprecation","datetime":"2024-05-13T08:54:12.070703+00:00","extra":{}}
[2024-05-13T08:54:12.070703+00:00] deprecation.INFO: User Deprecated: Since doctrine/doctrine-bundle 2.4: The "connection_override_options" connection parameter is deprecated {"exception":"[object] (ErrorException(code: 0): User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated at /tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83)"} []
{"message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","context":{"exception":{"class":"ErrorException","message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","code":0,"file":"/tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83"}},"level":200,"level_name":"INFO","channel":"deprecation","datetime":"2024-05-13T08:54:12.070731+00:00","extra":{}}
[2024-05-13T08:54:12.070731+00:00] deprecation.INFO: User Deprecated: Since doctrine/doctrine-bundle 2.4: The "connection_override_options" connection parameter is deprecated {"exception":"[object] (ErrorException(code: 0): User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated at /tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83)"} []
{"message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","context":{"exception":{"class":"ErrorException","message":"User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated","code":0,"file":"/tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83"}},"level":200,"level_name":"INFO","channel":"deprecation","datetime":"2024-05-13T08:54:12.070807+00:00","extra":{}}
[2024-05-13T08:54:12.070807+00:00] deprecation.INFO: User Deprecated: Since doctrine/doctrine-bundle 2.4: The "connection_override_options" connection parameter is deprecated {"exception":"[object] (ErrorException(code: 0): User Deprecated: Since doctrine/doctrine-bundle 2.4: The \"connection_override_options\" connection parameter is deprecated at /tmp/frankenphp_23f1d813c2733a7d5fcb222c88804dbf app.tar/vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php:83)"} []
{"message":"Checking for authenticator support.","context":{"firewall_name":"api","authenticators":1},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071432+00:00","extra":{}}
{"message":"Checking for authenticator support.","context":{"firewall_name":"api","authenticators":1},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071437+00:00","extra":{}}
{"message":"Checking support on authenticator.","context":{"firewall_name":"api","authenticator":"Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\Authenticator\\JWTAuthenticator"},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071447+00:00","extra":{}}
{"message":"Checking support on authenticator.","context":{"firewall_name":"api","authenticator":"Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\Authenticator\\JWTAuthenticator"},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071452+00:00","extra":{}}
{"message":"Checking for authenticator support.","context":{"firewall_name":"api","authenticators":1},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071453+00:00","extra":{}}
{"message":"Checking support on authenticator.","context":{"firewall_name":"api","authenticator":"Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\Authenticator\\JWTAuthenticator"},"level":100,"level_name":"DEBUG","channel":"security","datetime":"2024-05-13T08:54:12.071475+00:00","extra":{}}
[1] 3033805 segmentation fault sudo ./yc-api-binary php-server
I think you need to discover if it is coming from one of the Caddies or your php code.
I described the Caddies' config above. If there are any related issues w/ them, pls tell me, because I cannot see any.
Can you try to enable the debug symbols and use gdb to gather a stack trace?
Did you ever found a solution?
I also would like to proxy Caddy -> Caddy (FrankenPHP with Laravel Octane).
@francoism90 Do you also have a segfault?
@dunglas No, I get a 502 errors:
dec 16 12:08:33 desktop systemd-proxy[628387]: {"level":"error","ts":1734347313.729741,"logger":"http.log.error","msg":"remote error: tls: internal error","request":{"remote_ip":"10.89.6.55","remote_port":"45316","client_ip":"10.89.6.55","proto":"HTTP/2.0","method":"GET","host":"s>
dec 16 12:08:34 desktop systemd-proxy[628387]: {"level":"debug","ts":1734347314.09985,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"systemd-streamer:8443","total_upstreams":1}
dec 16 12:08:34 desktop systemd-proxy[628387]: {"level":"debug","ts":1734347314.1007457,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"systemd-streamer:8443","duration":0.000854515,"request":{"remote_ip":"10.89.6.55","remote_port":"45316","client_ip">
This is from HTTPS to HTTPS.
Can you share all your Caddyfiles please?
@dunglas
This is my Caddyfile of proxy:
{
debug
}
foo.lan {
tls internal
reverse_proxy https://systemd-foo:8443 {
transport http {
tls_insecure_skip_verify
}
}
}
This is the Caddyfile of the FrankenPHP container:
{
servers {
trusted_proxies static private_ranges
}
{$CADDY_GLOBAL_OPTIONS}
http_port {$HTTP_PORT:8080}
https_port {$HTTPS_PORT:8443}
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
frankenphp {
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
}
}
{$CADDY_SERVER_SERVER_NAME} {
log {
level {$CADDY_SERVER_LOG_LEVEL}
# Redact the authorization query parameter that can be set by Mercure...
format filter {
wrap {$CADDY_SERVER_LOGGER}
fields {
uri query {
replace authorization REDACTED
}
}
}
}
route {
root * "{$APP_PUBLIC_PATH}"
encode zstd br gzip
# Mercure configuration is injected here...
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
index frankenphp-worker.php
# Required for the public/storage/ directory...
resolve_root_symlink
}
}
}
You should disable or use internal TLS for the proxified services, and enable TLS only on the reverse proxy.
@dunglas This indeed works:
{
servers {
trusted_proxies static private_ranges
}
{$CADDY_GLOBAL_OPTIONS}
auto_https off // <-- this fixes the issue
http_port {$HTTP_PORT:8080}
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
frankenphp {
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
}
}
{$CADDY_SERVER_SERVER_NAME} {
log {
level {$CADDY_SERVER_LOG_LEVEL}
# Redact the authorization query parameter that can be set by Mercure...
format filter {
wrap {$CADDY_SERVER_LOGGER}
fields {
uri query {
replace authorization REDACTED
}
}
}
}
route {
root * "{$APP_PUBLIC_PATH}"
encode zstd br gzip
# Mercure configuration is injected here...
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
index frankenphp-worker.php
# Required for the public/storage/ directory...
resolve_root_symlink
}
}
}
I needed to change this, because it would keep using TLS on the FrankenPHP container.