FrankenPHP and Laravel Octane with Docker
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/frankenphp2 3RUN install-php-extensions pcntl4 5COPY . /app6 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!
If we use phpinfo()
, we should also see that FrankenPHP is our web server.