Chris
Chris White Web Developer

FrankenPHP and Laravel Octane with Docker

19 December 2023 ~3 minute read

FrankenPHP is a project I've been keeping an eye on for a while. It's an alternative way to run PHP applications on the web without using php-fpm, which makes it easier to deploy with Docker as you don't need to deploy multiple containers for both nginx and php-fpm, or deploy one container that runs both processes. It boasts a bunch of cool features, but the one I'm most interested in is worker mode. Worker mode does a similar job to RoadRunner or Swoole, where it keeps your application booted in memory and re-uses the same instance to serve multiple HTTP requests. Unlike php-fpm that tears down the world and builds it fresh for each request, this cuts out a lot of execution time especially if you're using frameworks like Symfony or Laravel that run a lot of bootstrapping code before getting to your actual application code. The only thing holding me back from using it so far was that Laravel Octane didn't have out-of-the-box support for FrankenPHP. That was until yesterday when The Laravel team announced the long-lived FrankenPHP PR to Octane finally got merged!

All of my projects are dockerized, and I wanted to show you how you can use FrankenPHP alongside Laravel Octane in Docker.

Update Laravel Octane

First thing's first, you need to update Octane to 2.2.3 to get the new FrankenPHP support. There was also a bug in earlier versions of 2.2.x which prevented Octane being used in a Docker container, so we want to at least go to 2.2.3.

1composer update laravel/octane:^2.2.3

Also re-run the Octane install command which will add the frankenphp-worker.php script to your public directory. This will be the "entrypoint" of your application and replaces public/index.php.

1php artisan octane:install

Octane will prompt you and want to download the FrankenPHP binary, you can say no because we'll be running FrankenPHP in Docker and don't need the binary in our project 🙂

Create a Dockerfile for the web server

Next, create the following Dockerfile.

1FROM dunglas/frankenphp
2 
3RUN install-php-extensions pcntl
4 
5COPY . /app
6 
7ENTRYPOINT ["php", "artisan", "octane:frankenphp"]

Nothing special here. We're using the dunglas/frankenphp image, installing the pcntl extension, copying our application source code into the container, and then starting the FrankenPHP web server via the octane:frankenphp artisan command.

The pcntl extension is required because Octane listens for SIGINT and SIGTERM signals. If you need other PHP extensions, add them to the list.

We're also starting FrankenPHP using octane:frankenphp instead of octane:start simply because I don't want to have to rely on OCTANE_SERVER=frankenphp being in my .env file, and I like it to be explicit that it's using FrankenPHP. If you inspect the source code of Octane, the octane:start command just calls octane:frankenphp if your application is configured to use FrankenPHP.

Update docker-compose.yml

Finally, in your docker-compose.yml file, use the new image:

1services:
2 web:
3 build:
4 context: .
5 dockerfile: infrastructure/web/Dockerfile
6 entrypoint: php artisan octane:frankenphp --max-requests=1
7 ports:
8 - "80:8000"
9 volumes:
10 - .:/app

This file exists at the root of my project directory, so context: . will ensure that the COPY in the Dockerfile copies the correct files. My Dockerfile exists at infrastructure/web/Dockerfile, so we also specify that path. Modify this if your Dockerfile exists elsewhere.

We override the entrypoint to add --max-requests=1. This is simply so that code changes take immediate effect. Octane does have a --watch flag to automatically reload the web server on changes to application code, but it requires installing Node inside the Docker image and using the chokidar npm package. I don't want my image to have Node, or install a package just to watch for files changing, so I just reload the server after every request instead. Note that this does negate the benefit of worker mode as your application won't remain booted between requests anymore, but only in your local development environment. When you deploy this image to production the --max-requests argument won't be present and you'll get the full speed benefits of worker mode.

By default Octane runs on port 8000, and I bind that to port 80 on my local machine just so I can access my app at http://localhost without specifying a port.

Well that was easy

Start up your container and go hit localhost in your browser. You should see your application!

Laravel welcome page

If we use phpinfo(), we should also see that FrankenPHP is our web server.

phpinfo

Made with Jigsaw and Torchlight.