Runtime (Octane + FrankenPHP)
Por que este serviço mantém processo PHP vivo entre requisições, e como isso muda o que o código pode fazer
php-fpm clássico: um processo, uma requisição, descarta tudo
O modelo tradicional de PHP em produção (php-fpm) inicia um processo PHP do zero — ou reaproveita um do pool sem estado nenhum entre usos — para cada requisição HTTP: carrega o framework, resolve o container de injeção de dependência, processa a requisição, devolve resposta, e descarta toda a memória daquele processo. Isso é caro (recarregar o framework inteiro a cada request tem custo real de CPU), mas tem uma vantagem que costuma passar despercebida até faltar: nada vaza de uma requisição para a próxima, porque o processo não sobrevive entre elas.
Octane: processo persistente
Laravel Octane inverte isso — mantém um número fixo de workers PHP vivos, o framework carregado uma vez na memória, e cada worker processa requisição após requisição sem recarregar nada. O ganho de performance vem exatamente daí: pular o boot do framework a cada request. O preço é que o modelo de “nada vaza” deixa de ser garantido pelo ambiente — vira responsabilidade do código.
Isso importa de verdade neste projeto: qualquer estado guardado numa propriedade estática, num singleton com estado mutável, ou uma variável de classe alterada durante uma requisição, sobrevive para a próxima requisição que aquele mesmo worker atender — inclusive se for de outro cliente, no caso de troca de contexto de filial/tenant. É por isso que padrão de “resolver uma vez, guardar na sessão da classe” que seria inofensivo em php-fpm vira bug intermitente (e potencialmente vazamento de dado entre requisições) sob Octane. ResolveBranchContext (ver concepts) resolve a filial ativa por requisição, não uma vez por worker — é esse tipo de cuidado que o modelo de Octane exige.
FrankenPHP: o servidor por trás do Octane aqui
Octane precisa de um servidor de aplicação que sustente esse modelo de worker persistente — as opções mais comuns são Swoole, RoadRunner e FrankenPHP. Este projeto usa FrankenPHP, que embute o interpretador PHP dentro do próprio binário do servidor Caddy (escrito em Go) — não é um proxy na frente de um PHP separado, é um processo único que fala HTTP e executa PHP.
# docker/deploy/supervisord.conf
[program:octane]
command=php /app/artisan octane:start --server=frankenphp --host=0.0.0.0 --port=8080 --admin-port=2019 --max-requests=500
--max-requests=500 é uma rede de segurança para o problema do parágrafo anterior em outra forma: memória que cresce devagar a cada requisição (um vazamento pequeno que php-fpm nunca deixaria acumular, porque reciclava o processo a cada request) eventualmente derruba um worker de vida muito longa. Reciclar o worker a cada 500 requisições processadas limita o quanto esse acúmulo pode crescer antes de ser descartado — sem isso, um vazamento lento só apareceria como OOM horas ou dias depois, difícil de reproduzir.
Os três processos do container, e por que supervisord
O container de produção não roda só o servidor HTTP — supervisord mantém três processos vivos simultaneamente:
[program:octane] # serve requisição HTTP
[program:queue] # php artisan queue:work --tries=3 --timeout=120 --max-time=3600
[program:scheduler] # php artisan schedule:work
queue:work processa jobs assíncronos (o worker de fila mencionado no chart do k3s-platform); schedule:work é o que efetivamente dispara fleet:evaluate-attention no horário agendado (ver commands) — sem esse processo rodando, o Schedule::command(...)->daily() declarado em routes/console.php nunca executaria sozinho, porque o scheduler do Laravel não é um cron embutido: precisa de algo chamando schedule:run (ou, aqui, schedule:work, que faz o mesmo em loop) periodicamente.