Nginx Unit with Laravel and PHP
Nginx Unit is a "Universal Web App Server" brought to you by Nginx. This is a web server that can "directly" communicate with your code base, helping you pass off HTTP requests to your code in a way it can understand.
It supports a bunch of languages and has a module for each supported language. That lets it treat each language specially, as needed.
For PHP support, it has a PHP module that creates PHP processes, similar(ish) to PHP-FPM, but without needing PHP-FPM. Getting rid of PHP-FPM sounds really rad to me, so I decided to see how it works.
Install PHP
We'll install PHP the usual way on any Ubuntu server - using the ppa:ondrej/php
repository. This is basically the de-facto way to use PHP on Ubuntu servers. It lets us get the latest PHP and install multiple versions of PHP on the same server (if so desired).
One issue with Unit: It expects the system-set default PHP version to be used. Luckily, we can recompile it's PHP module ourself (see here) to use our ppa:ondrej/php
-installed PHP.
First, we'll just install PHP as usual.
sudo add-apt-repository -y ppa:ondrej/php
sudo apt-get install -y php8.2-dev php8.2-embed \
php8.2-bcmath php8.2-cli php8.2-common php8.2-curl \
php8.2-gd php8.2-intl php8.2-mbstring php8.2-mysql php8.2-pgsql \
php8.2-redis php8.2-soap php8.2-sqlite3 php8.2-xml php8.2-zip
curl -sLS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/bin/ --filename=composer
Three things to note:
- We need
php8.2-dev
(the-dev
version) of PHP to get certain PHP "header" files, allowing us to later compile Unit's PHP module - We need
php8.2-embed
, as this is the SAPI that unit uses to spin up PHP processes. - We do NOT need
php-fpm
, and so we don't install it
Everything else is all the usual "stuff" for a PHP installation, typically seen on a Forge server.
Install Nginx Unit
We can install Nginx Unit as per their docs - nothing special to do there!
I used Ubuntu 22.04, and followed their instructions for that system:
sudo curl --output /usr/share/keyrings/nginx-keyring.gpg \
https://unit.nginx.org/keys/nginx-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/ubuntu/ jammy unit
deb-src [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/ubuntu/ jammy unit
" | sudo tee /etc/apt/sources.list.d/unit.list
sudo apt-get update
sudo apt-get install -y unit
I only installed unit
, opting NOT to install unit-dev
nor unit-php
yet, as we'll need to compile the PHP module ourself.
Manually Build Unit's PHP Module
Next we manually (so uncultured) re-build Unit's PHP module to work with ppa:ondrej/php
. This was taken from the aforementioned GitHub issue which helpfully pointed out how we can make this work.
Run the following as user root
:
cd /opt
# Latest version of Unit as of this release
VERSION="1.31.0"
curl -O https://unit.nginx.org/download/unit-$VERSION.tar.gz
tar xzf unit-$VERSION.tar.gz
cd unit-$VERSION
./configure --prefix=/usr --state=/var/lib/unit --control=unix:/var/run/control.unit.sock \
--pid=/var/run/unit.pid --log=/var/log/unit.log --tmp=/var/tmp --user=unit --group=unit \
--tests --openssl --modules=/usr/lib/unit/modules --libdir=/usr/lib/x86_64-linux-gnu
./configure php --module=php82 --config=php-config8.2
make php82
make install php82-install
# Restart
systemctl unit restart
# Check logs to ensure PHP module is loaded
cat /var/log/unit.log
We compiled the PHP module ourself, which uses the currently-installed PHP version (taken from the ppa:ondrej/php
repository). Success!
Create a Laravel App
In our case, we'll just directly create a new Laravel application on our server:
mkdir -p /var/www
cd /var/www
composer create-project laravel/laravel html
# Ensure files are owned by "unit", the user created
# by unit, so PHP can write to log files, etc
chown -R unit:unit /var/www/html
Easy enough!
Configure Unit
We can now configure Unit to run our application.
Create /var/www/unit.json
with:
{
"listeners": {
"*:80": {
"pass": "routes"
}
},
"routes": [
{
"match": {
"uri": "!/index.php"
},
"action": {
"share": "/var/www/html/public$uri",
"fallback": {
"pass": "applications/laravel"
}
}
}
],
"applications": {
"laravel": {
"type": "php",
"root": "/var/www/html/public/",
"script": "index.php",
"processes": {}
}
}
}
See the video for a full explanation of this configuration.
Then tell Unit to use this configuration to run the application:
sudo curl -X PUT --data-binary @unit.json --unix-socket \
/var/run/control.unit.sock http://localhost/config/
Head to your application (listening on port 80 currently)! Test it out with curl localhost
.
Control API
You can use the Control API to retrieve and update configuration.
Let's GET our configuration:
curl -X GET \
--unix-socket /var/run/control.unit.sock \
http://localhost/config/
Pros of Unit
We can remove PHP-FPM
! This is great, as it makes throwing our apps into a container a LOT simpler.
Unit also seems to be more efficient - I could NOT get it to break using ab
to send 100 requests at a time, with 10,000 total requests.
Cons of Unit
There are some trade-offs!
First, we can't switch PHP versions without re-compiling the Unit PHP module. This means it will be hard (impossible?) to run multiple versions of PHP at the same time while using Unit.
You might also need another HTTP layer in front of Unit (Nginx, Cloudflare, Cloudfront, fly.io's HTTP layer, etc). It turns out that Unit either makes some standard-ish configuration hard, or can't do it at all. Some examples:
- Gzip compression isn't (yet?) a thing for Unit
- It makes protecting dot files or other routes difficult
- It's not easy to set cache headers for static assets