đ Qbix Server : un serveur web Ă©crit en pur PHP

đ Introduction
Quand on veut sortir de PHP-FPM, le rĂ©flexe en 2026 câest FrankenPHP, Ă©ventuellement Swoole ou RoadRunner via Octane. Tous partagent une caractĂ©ristique : le serveur nâest pas Ă©crit en PHP. Go et Caddy pour lâun, une extension C pour lâautre.
Qbix Server prend le chemin inverse. Câest un serveur HTTP Ă©crit en PHP pur, lancĂ© par un simple php qbixserver.php, qui prĂ©tend remplacer nginx, php-fpm, supervisor et une partie de Redis ou Node.js par un seul process. Le projet est sous licence MIT, maintenu par Qbix sur GitHub, et vient de sortir en v2.0.0.
LâidĂ©e paraĂźt absurde au premier abord. Un serveur web en PHP, plus rapide que FPM ? En regardant de prĂšs, le mĂ©canisme tient debout, et il repose sur une fonctionnalitĂ© de lâOS que PHP-FPM exploite mal : le copy-on-write.
đ Comment un serveur en PHP peut-il tenir la charge ?
RĂ©ponse courte : en laissant le noyau faire le travail. Qbix charge le framework une fois dans un process parent, puis appelle pcntl_fork(). Chaque worker hĂ©rite des pages mĂ©moire du parent, marquĂ©es copy-on-write : il ne paie que ce quâil modifie. Selon le README, un worker coĂ»te environ 120 Ko au lieu des 30 Ă 60 Mo dâun worker FPM.
Câest lĂ que se joue lâargument. Sur une app qui passe son temps Ă attendre la base ou une API externe, le facteur limitant de FPM nâest pas le CPU mais le nombre de workers qui tiennent en RAM. Swoole rĂšgle ce problĂšme avec des coroutines, Ă condition dâutiliser ses clients I/O. FrankenPHP et RoadRunner gardent un modĂšle Ă nombre de workers fixe. Qbix garde du code PHP bloquant tout Ă fait classique et lance simplement beaucoup plus de workers : le projet annonce environ 5 000 workers concurrents sur 1 Go de RAM, contre une vingtaine pour FPM.
đ§± Deux modes dâexĂ©cution
Par dĂ©faut, Qbix tourne en workers persistants (le projet parle aussi de mode « octane »). Le worker survit entre les requĂȘtes, comme avec FrankenPHP en worker mode. Le problĂšme classique de ce modĂšle, ce sont les fuites dâĂ©tat : une propriĂ©tĂ© statique remplie par la requĂȘte A se retrouve dans la requĂȘte B.
Qbix attaque le problĂšme sur deux fronts. Dâabord, il réécrit Ă la volĂ©e, au chargement des fichiers, 27 fonctions natives qui nâont pas de sens en SAPI CLI : header(), setcookie(), session_start(), ini_set(), set_error_handler(), spl_autoload_register(), putenv()⊠Elles sont suivies par requĂȘte et remises Ă leur valeur de boot. Ensuite, entre deux requĂȘtes, un snapshot basĂ© sur Reflection restaure toutes les propriĂ©tĂ©s statiques (environ 0,03 ms), repeuple les superglobales et vide $_SESSION et les buffers de sortie.
Quand ça ne suffit pas, il reste le fork-per-request : chaque requĂȘte sâexĂ©cute dans un process forkĂ© qui meurt ensuite. Aucune fuite possible, puisque le process disparaĂźt. Câest plus lent, mais on conserve le bĂ©nĂ©fice du copy-on-write. Et on peut mĂ©langer les deux, en routant seulement certains chemins vers le mode fork :
"fork": { "patterns": ["legacy/.*", "unsafe-extension.php"] }đ DĂ©marrer avec Laravel ou WordPress
Pas de Composer à installer cÎté app. On clone le dépÎt et on pointe le serveur sur son projet :
git clone https://github.com/Qbix/webserver
cd my-laravel-app
php /path/to/webserver/qbixserver.php --root=public --preset=laravel --port=8080Pour WordPress, mĂȘme principe avec --root=. --preset=wordpress. Les presets rĂšglent les limites dâupload, la mĂ©moire et le GC des sessions (64M dâupload pour WordPress, calĂ© sur ce quâaffiche son uploader). La doc liste aussi Symfony, Drupal, Joomla et Magento comme compatibles sans modification.
Le composer.json du projet demande PHP 8.1 et ext-sockets. pcntl est prĂ©sentĂ© comme optionnel, mais sans lui on perd tout lâintĂ©rĂȘt du copy-on-write : sous Windows, Qbix se replie dâailleurs sur des sous-process php-cgi. Des binaires autonomes (PHP embarquĂ©) sont publiĂ©s pour Linux, macOS et Windows.
đł Un Dockerfile pour Laravel
Qbix ne publie pas dâimage Docker officielle. Voici celle que jâai montĂ©e et testĂ©e sur un Laravel 13 fraĂźchement installĂ©, avec Qbix v2.0.0 et PHP 8.4. Elle reprend le principe du build multi-stage pour Laravel : Composer dans une premiĂšre Ă©tape, un runtime lĂ©ger dans la seconde.
# Ătape 1 : dĂ©pendances Composer
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev --no-scripts
# Ătape 2 : runtime Qbix
FROM php:8.4-cli-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& docker-php-ext-install pcntl sockets pdo_mysql \
&& rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch v2.0.0 https://github.com/Qbix/webserver /opt/qbix
# Qbix v2.0.0 émet des notices Deprecated sous PHP 8.4 : on les garde hors des réponses
RUN printf 'display_errors=stderr\nerror_reporting=E_ALL & ~E_DEPRECATED\n' \
> /usr/local/etc/php/conf.d/qbix.ini
WORKDIR /app
COPY --from=vendor --chown=www-data:www-data /app /app
# Qbix écrit ses logs dans local/ et ses clés mesh dans keys/, relatifs au dossier courant
RUN mkdir -p local/logs keys && chown -R www-data:www-data local keys
USER www-data
EXPOSE 8080
HEALTHCHECK CMD curl -fs http://127.0.0.1:8080/up || exit 1
CMD ["php", "/opt/qbix/qbixserver.php", "--root=public", "--preset=laravel", "--port=8080", "--config=/app/qbix.json"]Et le fichier qbix.json Ă la racine du projet :
{
"Q": {
"webserver": {
"forkPerRequest": true
},
"dashboard": false
}
}Trois choix méritent une explication, parce que je les ai tous appris en cassant le conteneur.
Le forkPerRequest: true dâabord. En mode persistant, la premiĂšre requĂȘte passe, puis toutes les suivantes renvoient une 500 : Call to a member function handleRequest() on true. Le public/index.php de Laravel charge lâapplication avec require_once, et dans un worker qui a dĂ©jĂ exĂ©cutĂ© ce fichier, require_once renvoie true au lieu de lâobjet Application. En mode fork, chaque requĂȘte repart dâun process neuf et tout fonctionne. La promesse « Laravel sans modification » tient donc, mais pas dans le mode par dĂ©faut.
Ensuite "dashboard": false. Sans cette ligne, /Q/dashboard et /Q/health sont accessibles Ă nâimporte qui sur le port exposĂ©. Une fois le dashboard dĂ©sactivĂ©, /Q/health rĂ©pond 404, dâoĂč le healthcheck branchĂ© sur la route /up de Laravel. Dans mes tests, la page HTML de /Q/dashboard reste quand mĂȘme servie (sans ses donnĂ©es temps rĂ©el) : derriĂšre un reverse proxy, je bloquerais le prĂ©fixe /Q/ par prĂ©caution.
Enfin le fichier .ini. Sous PHP 8.4, Qbix v2.0.0 dĂ©clenche des notices « Creation of dynamic property » qui, avec la config par dĂ©faut de lâimage, atterrissent directement dans le corps des rĂ©ponses HTTP. Les renvoyer sur stderr suffit.
đ§° Ce quâil y a dans la boĂźte
Câest la partie qui mâa le plus surpris, parce quâon sort largement du rĂŽle de serveur HTTP :
- HTTP, WebSocket et SSE sur le mĂȘme port, avec des rooms Ă la Socket.IO ;
- les headers
X-Accel-Redirect(fichiers protĂ©gĂ©s servis par le serveur aprĂšs contrĂŽle dâaccĂšs, comme avec nginx),X-Cache-Treepour le cache de fragments, ETag et 304 ; - redimensionnement dâimages Ă la volĂ©e (
?w=300) avec négociation AVIF/WebP ; - TLS, cron et access log intégrés ;
- HTTP/2 via AMPHP en dépendance optionnelle.
La v2 ajoute une couche mesh (P2P chiffrĂ© sur Bluetooth, Wi-Fi ou TCP, synchronisation par filtres de Bloom et prolly trees) et un runtime iOS/Android. On parle lĂ dâun projet aux ambitions trĂšs larges, bien au-delĂ du simple remplacement de FPM.
đŻ Qbix vs FrankenPHP et Swoole
Sur le fonctionnement, les trois outils ne jouent pas du tout la mĂȘme partition :
| FrankenPHP | Swoole | Qbix Server | |
|---|---|---|---|
| Ăcrit en | Go + C (embarque PHP) | Extension C pour PHP | PHP pur |
| Installation | Binaire Go ou image Docker | pecl install swoole (compile du C) | php qbixserver.php, rien Ă installer |
| ModĂšle | Workers persistants | Coroutines dans des workers persistants | Workers persistants ou fork par requĂȘte (COW) |
| Fuites dâĂ©tat | Possibles, statiques Ă auditer | Possibles, globales Ă surveiller | Reset par snapshot, ou impossibles en fork |
| Code existant | Fonctionne dans la grande majorité | I/O bloquantes à réécrire | Fonctionne tel quel (27 fonctions réécrites) |
| HTTP/2 | Oui (Caddy), HTTP/3 aussi | Oui | Via AMPHP (optionnel) |
| WebSocket | Via Mercure | Intégré | Intégré, avec rooms |
X-Accel-Redirect | Non | Ă coder soi-mĂȘme | IntĂ©grĂ© |
Et les assets statiques ? Qbix les sert lui-mĂȘme, sans nginx devant : ETag et rĂ©ponses 304, compression, et redimensionnement dâimages Ă la volĂ©e (?w=300) avec conversion automatique en AVIF ou WebP selon le navigateur. Pour un site qui servait ses images via nginx plus un service de resize sĂ©parĂ©, câest une brique de moins.
Le projet publie aussi des chiffres de débit, que je reprends tels quels :
| Débit annoncé par Qbix | FrankenPHP | Swoole | Qbix Server |
|---|---|---|---|
| PHP, calcul pur (CPU-bound) | 350 req/s | ~400 req/s | 2 294 req/s (100 workers persistants) |
| PHP, 200 Mo de RAM, I/O de 50 ms | 78 req/s | ~300 req/s | 151 (fork) / 1 060 req/s (persistant) |
| PHP, 200 Mo de RAM, I/O de 200 ms | 20 req/s | ~200 Ă 500 req/s | 94 (fork) / 488 req/s (persistant) |
| Fichiers statiques | 10 038 req/s | 26 974 req/s | 18 311 req/s |
Ă lire avec rĂ©serve : ce sont des mesures maison, et un FrankenPHP Ă 350 req/s en CPU-bound laisse penser Ă une config peu reprĂ©sentative (mode classique plutĂŽt que worker mode ?). Notez dâailleurs que sur les statiques, Swoole reste devant. Ă reproduire sur sa propre app avant dây croire.
Ce qui me paraĂźt solide, câest lâargument structurel. En mĂ©moire, un worker forkĂ© en COW coĂ»te rĂ©ellement trĂšs peu, et sur de lâI/O bloquant, plus de workers donne mĂ©caniquement plus de dĂ©bit. CĂŽtĂ© compatibilitĂ©, Qbix nâexige aucune réécriture façon Swoole. En revanche, il nâoffre ni HTTP/3 ni lâĂ©cosystĂšme Caddy, et la communautĂ© reste confidentielle (autour de 160 Ă©toiles sur GitHub).
â ïž Quelques prĂ©cautions
Tout nâest pas remis Ă zĂ©ro entre deux requĂȘtes. Le worker reste vivant, donc certaines choses créées pendant une requĂȘte sont encore lĂ Ă la suivante. Qbix nettoie les variables de classe (les propriĂ©tĂ©s static) et les superglobales, mais pas le reste. ConcrĂštement :
- une constante créée avec
define()au milieu dâune requĂȘte existe toujours ensuite, et un seconddefine()du mĂȘme nom Ă©chouera ; - un fichier ouvert, une connexion Ă la base ou un handle cURL restent ouverts, avec leurs rĂ©glages ;
- une extension PHP écrite en C peut garder sa propre mémoire interne, que Qbix ne voit pas.
La rĂšgle pratique : tout ce que votre code ouvre ou dĂ©clare pendant une requĂȘte, il doit le refermer ou le rĂ©initialiser lui-mĂȘme. Câest la mĂȘme discipline quâavec Laravel Octane. Et pour le code qui ne la respecte pas, le mode fork-per-request rĂšgle la question.
OPcache. Le code réécrit est compilĂ© depuis la source transformĂ©e : la validation par fichier dâOPcache ne sâapplique pas. Sans hot reload activĂ©, il faut redĂ©marrer aprĂšs chaque dĂ©ploiement.
Output buffering. Du code qui vide tous les buffers en boucle avec ob_end_flush() peut casser la capture de la réponse.
TĂąches longues. Pour les migrations et scripts longs, la doc recommande --workers=1 ou, plus simplement, la CLI classique.
MaturitĂ©. Pas de package Packagist, une petite communautĂ©, un pĂ©rimĂštre qui sâĂ©tend vite. Pour de la prod critique, je testerais dâabord sur un service secondaire.
đ Conclusion
Qbix Server part dâune intuition juste : le modĂšle shared-nothing de PHP colle trĂšs bien au fork en copy-on-write, et câest la mĂ©moire par worker, plus que le CPU, qui plafonne FPM sur les apps I/O-bound. Le reset dâĂ©tat par réécriture de fonctions et snapshot Reflection est astucieux, et le mode fork-per-request offre une vraie porte de sortie.
Pour une app Laravel ou Symfony en production, je reste sur FrankenPHP, plus mĂ»r et mieux outillĂ©. Mais Qbix mĂ©rite un banc dâessai si vos workers passent leur vie Ă attendre des I/O, ou si lâidĂ©e de livrer une app PHP en un seul binaire vous parle. Et si vous le testez, gardez un Ćil sur la mĂ©moire rĂ©elle avec un outil comme Ember ou SigNoz : câest lĂ que les promesses se vĂ©rifient.
đ Liens utiles
/faq
Questions fréquentes
Qu'est-ce que Qbix Server ?
+
Qbix Server est un serveur web open source (licence MIT) Ă©crit entiĂšrement en PHP. Il se lance avec php qbixserver.php, sert les fichiers statiques, exĂ©cute l'application PHP dans des workers forkĂ©s et gĂšre HTTP, WebSocket et SSE sur le mĂȘme port, sans nginx ni PHP-FPM.
Comment Qbix Server Ă©vite-t-il les fuites dâĂ©tat entre requĂȘtes ?
+
En mode persistant (par dĂ©faut), il réécrit 27 fonctions natives (header, setcookie, session_start, ini_setâŠ) au chargement des fichiers et restaure les propriĂ©tĂ©s statiques via un snapshot Reflection entre deux requĂȘtes. Pour le code rĂ©calcitrant, le mode fork-per-request exĂ©cute chaque requĂȘte dans un process jetable.
Laravel fonctionne-t-il avec Qbix Server ?
+
Oui, avec le preset laravel, mais dans mes tests (Laravel 13, Qbix v2.0.0) uniquement en mode fork-per-request. En mode persistant par dĂ©faut, les requĂȘtes suivant la premiĂšre Ă©chouent, car le require_once de bootstrap/app.php renvoie true dans un worker rĂ©utilisĂ©.
Qbix Server ou FrankenPHP, lequel choisir ?
+
FrankenPHP reste le choix le plus mĂ»r pour une app Laravel ou Symfony en production, avec Caddy, HTTP/3 et une large communautĂ©. Qbix Server est intĂ©ressant quand la concurrence sur des requĂȘtes I/O-bound est le goulet, ou pour distribuer une app PHP en binaire unique, mais ses benchmarks sont publiĂ©s par le projet lui-mĂȘme et mĂ©ritent d'ĂȘtre reproduits.