Nik Spyratos

Weekend tinkering: Building FrankenPHP Laravel binaries

For those who don't know of it, FrankenPHP is a great all-in-one tool for PHP apps: it contains a very fast PHP runner (fpm alternative, similar to Swoole & Roadrunner), as well as being bundled with the Caddy webserver, all in one binary file! It's a great example of the synergy between Golang and PHP.

I'm on an M1 mac, using Laravel Herd.

This post is a log of progress and issues I've had.

It's NOT an attempt to complain, but just show the process of figuring things out and learning.

The goals here are:

Documentation: Building Laravel binaries

First challenge: Seems the default PHP extension list is small and you'll need to add your project's extensions to the static-build.Dockerfile.

No biggie, but I'd probably add a reminder in the docs to do this.

All I needed to add was pdo and sqlite.

I noticed that FrankenPHP builds use Alpine, because Alpine using musl libc results in smaller binary sizes.

This is in contrast to the creator's (great) article on PHP app containerisation tips which recommends against Alpine due to potential compatibility issues..

Lesson: There's always a good exception!

Recommended read: Containerisation for PHP apps

After that, the build started going...

and going...

and going...

Total build time: 81 minutes! Apparently this is due to upx, so another time I'll try without it and see how it goes. For prod deployments however it'd be more ideal to use it, but likely on a build server instead.

Next: macOS local build. I don't use Docker or a VM for my projects, so it'd be ideal to have a native build to run on my machine.

First issue: the build script is not finding the hash extension somehow, despite it being built-in to php since 7.4?

Since the host OS type builds rely on cloning FrankenPHP directly and running the build script, we can luckily modify that file 😊

The problem seems to be that calls passed to static-php-cli have the list of your project's extension dependencies passed through to it.

This can include core PHP extensions like hash, json, pcre etc.

static-php-cli does not like it when you pass builtins to its extension list for the spc download command. Laravel and other projects, especially if they've existed for many PHP versions, will be likely to include these extensions as dependencies, from when they were not included in the PHP core.

What I'm interested in is why this wasn't an issue in the Docker build? My dependencies are the same. Question for another time.

I hard coded the output of my project's extensions in the build script, sans the builtins (in my case hash, json, pcre) and it started going again. Onwards!

Next problem: Build is failing, because it seems to be looking for libraries in a nested way that is duplicating the path. E.g. Users/nik/.../project/lib//Users/nik/project.../lib.

Now I'm not clued up as to everything going on in this script, so I put the commands being run and my errors into Claude AI. It recommended removing a sed command in the CGO_CFLAGS parts of the script, which worked.

So the build started running! Didn't finish though.

Now, the error is fatal error: 'Zend/zend_types.h' file not found. Github shows at least one other person had this problem but for xcaddy builds instead of embedded app builds like mine. The recommended fix was to go download the PHP sources and try again.

My understanding is that that should only be needed for more complex build requirements, and shouldn't be needed for average PHP app binary builds (I think?).

I decided to pause the project here, as this was now going down a rabbit hole of potential complexity I didn't want. I'm not much of a Go or C dev, so everything going on here is a bit above my head!

Getting at least one build done is a success! I can tackle the others over time as the project and my understanding matures.

The above was most the original piece I posted. During the following week, I was able to successfully get a build running after all!

After writing my post, I submitted a Github issue to the FrankenPHP repo outlining the issues I'd had.

After some help from Kévin Dunglas, Rob Landers, and Jerry Ma, I got a successful macOS binary of Toybox built and running!

For comparison, the compressed Linux binary was 108MB, whereas the uncompressed macOS binary was ~500MB. Quite hefty!

All issues were traced down to how extensions are passed through from your app into the FrankenPHP build command. Unfortunately, relying on Composer's get-platform-reqs command will often have inaccurate extension requirements, either due to packages listing dependencies that are unneeded or long-since included in PHP core, or just not listing required dependencies properly.

Luckily, you can override which extensions to include by overriding the PHP_EXTENSIONS variable that is parsed by the build script. Or, just use the Dockerfile as intended!

I still love FrankenPHP as an Octane driver and Caddy replacement all-in-one! But I get the impression that the binary builds are still a bit finicky.

Massive shoutout to Kévin for being the mad lad to create this project in the first place!

#frankenphp #laravel #niksoftware #php