{"id":1794,"date":"2026-10-05T14:05:17","date_gmt":"2026-10-05T17:05:17","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1794"},"modified":"2026-10-05T14:05:17","modified_gmt":"2026-10-05T17:05:17","slug":"frankenphp-php-com-caddy-embutido-e-worker-mode-no-linux","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/10\/frankenphp-php-com-caddy-embutido-e-worker-mode-no-linux\/","title":{"rendered":"FrankenPHP: PHP with embedded Caddy and worker mode on Linux"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" alt=\"Mascote do LinuxPro apertando com uma chave inglesa o parafuso de um servidor com o logo do FrankenPHP, enquanto o elefante verde do FrankenPHP segura o outro lado e o cachorro caramelo observa\" src=\"\/wp-content\/uploads\/2026\/10\/frankenphp-servidor-php-caddy.webp\" width=\"1486\" height=\"856\" \/><\/p>\n<p>Por muito tempo, servir PHP em produ\u00e7\u00e3o quis dizer duas pe\u00e7as: um servidor web na frente (Apache ou nginx) e o PHP-FPM atr\u00e1s, conversando por FastCGI, cada um com sua configura\u00e7\u00e3o, seu servi\u00e7o e seus logs. O <a href=\"https:\/\/frankenphp.dev\/\">FrankenPHP<\/a> junta tudo num bin\u00e1rio: o servidor web <a href=\"\/2026\/10\/caddy-no-linux-servidor-web-com-https-automatico\/\">Caddy<\/a> com o interpretador PHP embutido. Voc\u00ea ganha HTTPS autom\u00e1tico, HTTP\/3 e, de quebra, um <em>worker mode<\/em> que mant\u00e9m a aplica\u00e7\u00e3o carregada na mem\u00f3ria entre requisi\u00e7\u00f5es. Este guia explica o que \u00e9, instala no Debian e no Ubuntu pelos pacotes oficiais e mostra os dois modos funcionando.<\/p>\n<h2>O que \u00e9 o FrankenPHP<\/h2>\n<p>O FrankenPHP \u00e9 um servidor de aplica\u00e7\u00f5es PHP escrito em Go, criado por K\u00e9vin Dunglas \u2014 o autor do API Platform \u2014 com patroc\u00ednio da cooperativa Les-Tilleuls.coop. A vers\u00e3o 1.0 saiu em dezembro de 2023. Em <a href=\"https:\/\/thephp.foundation\/blog\/2025\/05\/15\/frankenphp\/\">maio de 2025<\/a>, a PHP Foundation passou a apoiar oficialmente o projeto e o c\u00f3digo foi para a organiza\u00e7\u00e3o oficial do PHP no GitHub (<a href=\"https:\/\/github.com\/php\/frankenphp\">php\/frankenphp<\/a>), com a governan\u00e7a mantida pelos mantenedores originais. A licen\u00e7a \u00e9 MIT.<\/p>\n<p>A vers\u00e3o atual \u00e9 a <a href=\"https:\/\/github.com\/php\/frankenphp\/releases\/tag\/v1.13.0\">1.13.0<\/a>, de 4 de outubro de 2026, que embute o Caddy 2.11.7 e o Mercure 1.0 e corrige cinco falhas de seguran\u00e7a (duas classificadas como altas). O nome \u00e9 literal: \u00e9 um PHP costurado dentro do Caddy, da\u00ed o elefante com parafusos no logo.<\/p>\n<p>O que ele traz:<\/p>\n<ul>\n<li><strong>Um processo s\u00f3.<\/strong> O PHP roda em threads dentro do processo do Caddy, sem FastCGI e sem um pool separado para administrar. Por isso o FrankenPHP usa o PHP compilado em modo ZTS (<em>thread safe<\/em>).<\/li>\n<li><strong>Tudo o que o Caddy faz.<\/strong> Certificado autom\u00e1tico, HTTP\/2, HTTP\/3, compress\u00e3o zstd\/brotli\/gzip, proxy reverso e o mesmo Caddyfile.<\/li>\n<li><strong>Worker mode.<\/strong> A aplica\u00e7\u00e3o inicializa uma vez e atende as requisi\u00e7\u00f5es seguintes j\u00e1 carregada. O Laravel (via Octane) e o Symfony (via Runtime) t\u00eam integra\u00e7\u00e3o oficial.<\/li>\n<li><strong>Extras de aplica\u00e7\u00e3o web.<\/strong> Early Hints (status 103), tempo real com Mercure, <em>hot reload<\/em> e X-Sendfile para arquivos grandes.<\/li>\n<\/ul>\n<figure><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/frankenphp-nginx-phpfpm-vs-worker.webp\" alt=\"Diagrama comparando a pilha cl\u00e1ssica, com nginx na frente e um pool de php-fpm separado ligado por FastCGI, onde cada requisi\u00e7\u00e3o recarrega a aplica\u00e7\u00e3o, com o FrankenPHP, um \u00fanico processo com Caddy e PHP ZTS, que oferece o modo cl\u00e1ssico e o modo worker, no qual a aplica\u00e7\u00e3o inicializa uma vez e fica na mem\u00f3ria\" width=\"1200\" height=\"660\" \/><figcaption>\u00c0 esquerda, dois servi\u00e7os e um socket FastCGI. \u00c0 direita, um bin\u00e1rio, um Caddyfile e um servi\u00e7o \u2014 e a op\u00e7\u00e3o de manter a aplica\u00e7\u00e3o na mem\u00f3ria.<\/figcaption><\/figure>\n<h2>Instalando no Debian e no Ubuntu<\/h2>\n<p>H\u00e1 tr\u00eas caminhos: pacotes <code>.deb<\/code>\/<code>.rpm<\/code>, bin\u00e1rio est\u00e1tico e Docker. Para servidor, o pacote \u00e9 o mais pr\u00e1tico, porque traz servi\u00e7o systemd, <code>php.ini<\/code> de produ\u00e7\u00e3o e extens\u00f5es instal\u00e1veis pelo apt. Os pacotes s\u00e3o mantidos pela equipe do projeto num reposit\u00f3rio pr\u00f3prio, com uma variante por vers\u00e3o do PHP (8.2 a 8.5):<\/p>\n<pre><code class=\"language-bash\">sudo apt install -y curl ca-certificates\nsudo mkdir -p \/etc\/apt\/keyrings\n\nVERSION=85   # 82, 83, 84 ou 85\nsudo curl -fsS https:\/\/pkg.henderkes.com\/api\/packages\/${VERSION}\/debian\/repository.key \\\n  -o \/etc\/apt\/keyrings\/static-php${VERSION}.asc\necho \"deb [signed-by=\/etc\/apt\/keyrings\/static-php${VERSION}.asc] https:\/\/pkg.henderkes.com\/api\/packages\/${VERSION}\/debian php-zts main\" \\\n  | sudo tee \/etc\/apt\/sources.list.d\/static-php${VERSION}.list\nsudo apt update\nsudo apt install frankenphp<\/code><\/pre>\n<p>Testei esses comandos num Debian 13 limpo. O resultado:<\/p>\n<pre><code class=\"language-bash\">$ frankenphp version\nFrankenPHP v1.13.0 PHP 8.5 Caddy v2.11.7\n\n$ php-zts -v\nPHP 8.5.11 (cli) (ZTS gcc 16.2.0 x86_64)<\/code><\/pre>\n<p>O pacote cria o usu\u00e1rio <code>frankenphp<\/code> (membro do grupo <code>www-data<\/code>), instala o servi\u00e7o <code>frankenphp.service<\/code> e o CLI <code>php-zts<\/code>. Use esse CLI para Composer e scripts: na 1.13, o subcomando <code>frankenphp php-cli<\/code> passou a usar a SAPI de linha de comando nativa do PHP e, com PHP abaixo de 8.6, recusa rodar com a mensagem <em>this functionality is not available<\/em>.<\/p>\n<p>Os arquivos ficam em:<\/p>\n<table>\n<thead>\n<tr>\n<th>Item<\/th>\n<th>Caminho (pacote deb\/rpm)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Configura\u00e7\u00e3o do servidor<\/td>\n<td><code>\/etc\/frankenphp\/Caddyfile<\/code><\/td>\n<\/tr>\n<tr>\n<td>Configura\u00e7\u00f5es extras (carregadas sozinhas)<\/td>\n<td><code>\/etc\/frankenphp\/Caddyfile.d\/*.caddyfile<\/code><\/td>\n<\/tr>\n<tr>\n<td>php.ini (j\u00e1 com preset de produ\u00e7\u00e3o)<\/td>\n<td><code>\/etc\/php-zts\/php.ini<\/code><\/td>\n<\/tr>\n<tr>\n<td>ini adicionais<\/td>\n<td><code>\/etc\/php-zts\/conf.d\/*.ini<\/code><\/td>\n<\/tr>\n<tr>\n<td>Site de exemplo<\/td>\n<td><code>\/usr\/share\/frankenphp\/<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Extens\u00f5es entram pelo apt com o prefixo <code>php-zts-<\/code>. A instala\u00e7\u00e3o base j\u00e1 traz OPcache, mbstring, curl, sodium, dom e outras; o resto se instala assim:<\/p>\n<pre><code class=\"language-bash\">apt-cache search php-zts- | grep -v debuginfo\nsudo apt install php-zts-pdo-mysql php-zts-gd php-zts-intl php-zts-redis\nphp-zts -m<\/code><\/pre>\n<p>Prefere n\u00e3o adicionar reposit\u00f3rio? O script oficial baixa o bin\u00e1rio est\u00e1tico, que roda em qualquer distribui\u00e7\u00e3o sem depend\u00eancias: <code>curl https:\/\/frankenphp.dev\/install.sh | sh<\/code>. A troca \u00e9 que extens\u00f5es passam a ter de ser compiladas dentro do bin\u00e1rio.<\/p>\n<h2>Modo cl\u00e1ssico: substituindo nginx + PHP-FPM<\/h2>\n<p>No modo cl\u00e1ssico, o FrankenPHP se comporta como a dupla tradicional: cada requisi\u00e7\u00e3o carrega o script do zero. \u00c9 o modo para qualquer aplica\u00e7\u00e3o PHP existente \u2014 WordPress, Nextcloud, um sistema legado. O Caddyfile de um site em produ\u00e7\u00e3o:<\/p>\n<pre><code class=\"language-text\">{\n\tfrankenphp\n}\n\napp.exemplo.com.br {\n\troot \/var\/www\/app\/public\n\tencode zstd br gzip\n\tphp_server\n\tlog\n}<\/code><\/pre>\n<p>A diretiva <code>php_server<\/code> resume o que no nginx exigiria <code>try_files<\/code>, <code>location ~ \\.php$<\/code> e <code>fastcgi_pass<\/code>: serve arquivos est\u00e1ticos, manda <code>.php<\/code> para o interpretador e redireciona o resto para o <code>index.php<\/code>. Como no Caddy, o dom\u00ednio no endere\u00e7o do bloco basta para o certificado ser emitido e renovado sozinho, com DNS apontado e portas 80 e 443 abertas.<\/p>\n<pre><code class=\"language-bash\">sudo frankenphp fmt --overwrite \/etc\/frankenphp\/Caddyfile\nsudo frankenphp validate --config \/etc\/frankenphp\/Caddyfile\nsudo systemctl enable --now frankenphp\nsudo systemctl reload frankenphp\njournalctl -u frankenphp -f<\/code><\/pre>\n<p>Para testar sem tocar no servi\u00e7o, rode na pasta do projeto <code>frankenphp php-server<\/code>: ele serve o diret\u00f3rio atual na hora. Para WordPress, a <a href=\"https:\/\/frankenphp.dev\/docs\/wordpress\/\">documenta\u00e7\u00e3o oficial<\/a> recomenda um bloco equivalente: dom\u00ednio, <code>php_server<\/code>, compress\u00e3o e log.<\/p>\n<h2>Worker mode: a aplica\u00e7\u00e3o fica na mem\u00f3ria<\/h2>\n<p>Num framework moderno, boa parte do tempo de cada requisi\u00e7\u00e3o vai para a inicializa\u00e7\u00e3o: autoload, leitura de configura\u00e7\u00e3o, montagem do container de depend\u00eancias. No worker mode, o script de entrada faz isso uma vez e entra num la\u00e7o que atende requisi\u00e7\u00e3o ap\u00f3s requisi\u00e7\u00e3o com <code>frankenphp_handle_request()<\/code>. As superglobais (<code>$_GET<\/code>, <code>$_POST<\/code>, <code>$_SERVER<\/code>) s\u00e3o renovadas a cada volta; o resto continua carregado.<\/p>\n<p>Um worker m\u00ednimo, para ver o efeito:<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\/\/ \/srv\/app\/worker.php\n$boot = microtime(true);   \/\/ executa uma \u00fanica vez\n$count = 0;\n\n$handler = function () use (&amp;$count, $boot) {\n    $count++;\n    echo \"requisi\u00e7\u00e3o $count, app iniciada em \" . round($boot) . \"\\n\";\n};\n\nwhile (frankenphp_handle_request($handler)) {\n    gc_collect_cycles();\n}<\/code><\/pre>\n<pre><code class=\"language-text\">{\n\tfrankenphp {\n\t\tworker {\n\t\t\tfile \/srv\/app\/worker.php\n\t\t\tnum 4\n\t\t}\n\t}\n}\n\napp.exemplo.com.br {\n\troot \/srv\/app\n\tphp_server\n}<\/code><\/pre>\n<p>No teste, tr\u00eas requisi\u00e7\u00f5es seguidas responderam <code>requisi\u00e7\u00e3o 1<\/code>, <code>2<\/code> e <code>3<\/code>, todas com o mesmo hor\u00e1rio de inicializa\u00e7\u00e3o: o estado sobreviveu entre requisi\u00e7\u00f5es. No modo cl\u00e1ssico, o mesmo contador voltaria sempre a 1.<\/p>\n<p>Isso \u00e9 poderoso e cobra disciplina. Vari\u00e1veis est\u00e1ticas, singletons e conex\u00f5es persistem entre usu\u00e1rios; vazamento de mem\u00f3ria se acumula; uma exce\u00e7\u00e3o n\u00e3o tratada derruba o worker. Por isso a documenta\u00e7\u00e3o recomenda capturar exce\u00e7\u00f5es dentro do <em>handler<\/em>, e o FrankenPHP reinicia workers que falham (o limite \u00e9 configur\u00e1vel com <code>max_consecutive_failures<\/code>). Para mudan\u00e7as de c\u00f3digo em desenvolvimento, a op\u00e7\u00e3o <code>watch<\/code> reinicia o worker quando arquivos mudam.<\/p>\n<h3>Laravel e Symfony<\/h3>\n<p>Voc\u00ea n\u00e3o precisa escrever o la\u00e7o \u00e0 m\u00e3o. No Laravel, o Octane cuida disso:<\/p>\n<pre><code class=\"language-bash\">composer require laravel\/octane\nphp-zts artisan octane:install --server=frankenphp\nphp-zts artisan octane:frankenphp<\/code><\/pre>\n<p>No Symfony, o componente Runtime tem suporte nativo; a configura\u00e7\u00e3o est\u00e1 na <a href=\"https:\/\/frankenphp.dev\/docs\/symfony\/\">p\u00e1gina do Symfony<\/a> da documenta\u00e7\u00e3o. Em ambos, rode os testes da aplica\u00e7\u00e3o em worker mode antes de ir para produ\u00e7\u00e3o \u2014 c\u00f3digo que assume \u201cprocesso novo a cada requisi\u00e7\u00e3o\u201d aparece r\u00e1pido.<\/p>\n<h2>Docker<\/h2>\n<p>A imagem oficial \u00e9 <code>dunglas\/frankenphp<\/code>, com variantes por vers\u00e3o do PHP e base Debian ou Alpine. Para subir o diret\u00f3rio atual:<\/p>\n<pre><code class=\"language-bash\">docker run -v $PWD:\/app\/public \\\n  -p 80:80 -p 443:443 -p 443:443\/udp \\\n  dunglas\/frankenphp:1.13-php8.5<\/code><\/pre>\n<p>Abra <code>https:\/\/localhost<\/code> (n\u00e3o <code>https:\/\/127.0.0.1<\/code>, para o qual n\u00e3o h\u00e1 certificado por padr\u00e3o) e aceite o certificado local. Para uma imagem de produ\u00e7\u00e3o, as extens\u00f5es entram com o <code>install-php-extensions<\/code> que j\u00e1 vem na base, e o worker com a vari\u00e1vel <code>FRANKENPHP_CONFIG<\/code>:<\/p>\n<pre><code class=\"language-dockerfile\">FROM dunglas\/frankenphp:1.13-php8.5\nRUN install-php-extensions pdo_mysql gd intl zip opcache\nRUN cp $PHP_INI_DIR\/php.ini-production $PHP_INI_DIR\/php.ini\nCOPY . \/app\nENV FRANKENPHP_CONFIG=\"worker .\/public\/index.php\"<\/code><\/pre>\n<p>Prefira as imagens baseadas em Debian. As Alpine e o bin\u00e1rio est\u00e1tico usam a musl libc, e a documenta\u00e7\u00e3o lista incompatibilidades, como a falta da flag <code>GLOB_BRACE<\/code> do <code>glob()<\/code>. Para o b\u00e1sico de containers, veja o <a href=\"\/2017\/08\/curso-de-docker-gratis\/\">curso de Docker<\/a>.<\/p>\n<h2>Cuidados antes de migrar<\/h2>\n<ul>\n<li><strong>Extens\u00f5es n\u00e3o <em>thread safe<\/em>.<\/strong> <code>imap<\/code>, o agente do New Relic e o <code>pcov<\/code> n\u00e3o funcionam; o <code>imagick<\/code> conflita com as threads do ImageMagick, e a documenta\u00e7\u00e3o manda desligar essas threads; os perfiladores do Datadog e do Blackfire ainda t\u00eam limita\u00e7\u00f5es. A lista completa est\u00e1 em <a href=\"https:\/\/frankenphp.dev\/docs\/known-issues\/\">Known issues<\/a>.<\/li>\n<li><strong>Threads, n\u00e3o processos.<\/strong> O padr\u00e3o \u00e9 o dobro do n\u00famero de CPUs. Na 1.13, <code>num_threads<\/code> passou a contar s\u00f3 as threads para requisi\u00e7\u00f5es fora dos workers, que entram por cima; quem fixava esse valor junto com workers deve revisar a configura\u00e7\u00e3o antes de atualizar.<\/li>\n<li><strong>Worker mode \u00e9 opcional.<\/strong> Comece no modo cl\u00e1ssico, que n\u00e3o muda nada no comportamento da aplica\u00e7\u00e3o, e s\u00f3 ative o worker depois de testar.<\/li>\n<li><strong>Mantenha-se atualizado.<\/strong> A 1.13 corrigiu, entre outras, uma falsifica\u00e7\u00e3o de cabe\u00e7alho por nomes com ponto e um <code>putenv()<\/code> que vazava valores entre requisi\u00e7\u00f5es. Atualizar \u00e9 <code>sudo apt upgrade<\/code>.<\/li>\n<\/ul>\n<p>Se voc\u00ea mant\u00e9m v\u00e1rias vers\u00f5es de PHP lado a lado com nginx, o post <a href=\"\/2026\/09\/nginx-varias-versoes-php-debian-ubuntu\/\">Nginx e v\u00e1rias vers\u00f5es de PHP<\/a> mostra a abordagem tradicional \u2014 \u00fatil para comparar e para as aplica\u00e7\u00f5es que ainda dependem de uma extens\u00e3o incompat\u00edvel.<\/p>\n<h2>Links<\/h2>\n<ul>\n<li><a href=\"https:\/\/frankenphp.dev\/\">frankenphp.dev<\/a> \u00b7 <a href=\"https:\/\/frankenphp.dev\/docs\/\">documenta\u00e7\u00e3o<\/a> \u00b7 <a href=\"https:\/\/github.com\/php\/frankenphp\">github.com\/php\/frankenphp<\/a><\/li>\n<li><a href=\"https:\/\/frankenphp.dev\/docs\/worker\/\">Worker mode<\/a> \u00b7 <a href=\"https:\/\/frankenphp.dev\/docs\/config\/\">Configura\u00e7\u00e3o<\/a> \u00b7 <a href=\"https:\/\/frankenphp.dev\/docs\/laravel\/\">Laravel<\/a> \u00b7 <a href=\"https:\/\/frankenphp.dev\/docs\/production\/\">Produ\u00e7\u00e3o<\/a><\/li>\n<li>Aqui no blog: <a href=\"\/2026\/10\/caddy-no-linux-servidor-web-com-https-automatico\/\">Caddy no Linux<\/a> \u00b7 <a href=\"\/2026\/09\/a-historia-da-linguagem-go\/\">A hist\u00f3ria da linguagem Go<\/a><\/li>\n<\/ul>\n<p>O FrankenPHP n\u00e3o pede que voc\u00ea reescreva nada para come\u00e7ar: no modo cl\u00e1ssico, ele simplesmente troca dois servi\u00e7os por um e traz o HTTPS autom\u00e1tico do Caddy junto. O ganho grande vem depois, com o worker mode \u2014 e a\u00ed vale tratar a migra\u00e7\u00e3o como mudan\u00e7a de arquitetura, com testes, e n\u00e3o como troca de servidor.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>FrankenPHP combines Caddy and PHP into a single binary: automatic HTTPS, HTTP\/3 and worker mode. Installation on Debian and Ubuntu, classic mode, tested worker mode, Docker and precautions before migrating.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11,45,2,111,120,3],"tags":[524,527,14,525,528,237,529],"class_list":["post-1794","post","type-post","status-publish","format-standard","hentry","category-debian","category-desenv","category-linux","category-opensource","category-servidores","category-ubuntu","tag-caddy","tag-frankenphp","tag-howto","tag-https","tag-laravel","tag-php","tag-symfony"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1794","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1794"}],"version-history":[{"count":1,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1794\/revisions"}],"predecessor-version":[{"id":1795,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1794\/revisions\/1795"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1794"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1794"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1794"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}