frankenphp icon indicating copy to clipboard operation
frankenphp copied to clipboard

Proxying FrankenPHP to Caddy (technically Caddy to Caddy) not working

Open 7system7 opened this issue 1 year ago • 3 comments
trafficstars

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

image

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.

7system7 avatar May 12 '24 11:05 7system7

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.

withinboredom avatar May 12 '24 12:05 withinboredom

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.

7system7 avatar May 13 '24 09:05 7system7

Can you try to enable the debug symbols and use gdb to gather a stack trace?

dunglas avatar May 13 '24 10:05 dunglas

Did you ever found a solution?

I also would like to proxy Caddy -> Caddy (FrankenPHP with Laravel Octane).

francoism90 avatar Dec 16 '24 10:12 francoism90

@francoism90 Do you also have a segfault?

dunglas avatar Dec 16 '24 11:12 dunglas

@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.

francoism90 avatar Dec 16 '24 11:12 francoism90

Can you share all your Caddyfiles please?

dunglas avatar Dec 16 '24 11:12 dunglas

@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
		}
	}
}

francoism90 avatar Dec 16 '24 12:12 francoism90

You should disable or use internal TLS for the proxified services, and enable TLS only on the reverse proxy.

dunglas avatar Dec 16 '24 15:12 dunglas

@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.

francoism90 avatar Dec 16 '24 17:12 francoism90