PHP O Servidor Embutido (php -S)
Para ver uma página PHP no navegador, alguém precisa fazer o papel de servidor web: receber o pedido do navegador, chamar o PHP e devolver o HTML. Você não precisa instalar Apache nem nginx para isso: o próprio PHP traz um servidor pequeno, ligado com um comando só, php -S. Nesta aula você vai ligá-lo, ler o que ele mostra, escolher a pasta do site, criar um roteador e desligá-lo.
- Ligar o servidor com
php -S localhost:8000e abrir as páginas no navegador. - Ler a mensagem de início e o registro (log) de cada visita.
- Escolher a pasta do site com
-t. - Usar um script roteador para endereços "bonitos" como
/contato. - Parar o servidor com Ctrl+C.
- Resolver "Address in use", "Permission denied" e companhia.
- Por que ele é só para desenvolvimento, e o que se usa em produção.
A ideia: um balcão provisório
Lembra do restaurante da aula Como o PHP Funciona? O navegador é o cliente, o servidor web é o garçom e o PHP é a cozinha. O php -S é um garçom de treinamento: atende uma mesa de cada vez, sem pressa, e anota tudo num caderno que você pode ler. Perfeito para testar em casa; péssimo para um restaurante cheio.
Você precisa do PHP instalado no seu computador para esta aula (veja Instalar no Linux ou Instalar no Windows e macOS). As caixas Experimente em modo web fazem o papel do servidor dentro do navegador, e aparecem no fim de cada seção.
Ligando o servidor
A Ana tem uma pasta site com o arquivo ola.php (o mesmo da aula Olá, Mundo). Ela entra na pasta e liga o servidor:
$ cd site $ php -S localhost:8000 [Fri Oct 16 20:43:26 2026] PHP 8.5.8 Development Server (http://localhost:8000) started
- php
- O mesmo programa de sempre.
- -S
- De Server: "funcione como servidor web". O S é maiúsculo; o
-sminúsculo é outra coisa. - localhost
- O endereço em que o servidor vai escutar: "este computador". Só programas da própria máquina conseguem acessar.
- :8000
- A porta: o número da "porta de entrada" do servidor. Qualquer número de 1024 a 65535 que esteja livre serve; 8000 e 8080 são os costumeiros.
E a resposta, parte por parte:
- [Fri Oct 16 20:43:26 2026]
- Data e hora, em inglês: sexta-feira (Friday), 16 de outubro (October) de 2026, às 20:43:26.
- PHP 8.5.8 Development Server
- "Servidor de desenvolvimento do PHP 8.5.8". O nome já avisa: é para desenvolver.
- (http://localhost:8000)
- O endereço para abrir no navegador.
- started
- "Iniciado". Deu certo.
Repare que o prompt $ não voltou. O servidor fica rodando, ocupando aquele terminal, esperando visitas. É assim mesmo: deixe essa janela aberta e use outra janela (ou outra aba) do terminal para os demais comandos.
Abrindo no navegador
Abra o navegador e digite na barra de endereços:
http://localhost:8000/ola.php
A página mostra Olá, mundo!. O caminho depois da porta (/ola.php) é o nome do arquivo dentro da pasta onde você ligou o servidor. Um arquivo site/fotos/praia.jpg seria aberto em http://localhost:8000/fotos/praia.jpg.
No terminal, dá para fazer o mesmo pedido com o curl (numa outra janela do terminal, já que a primeira está ocupada pelo servidor):
$ curl localhost:8000/ola.php Olá, mundo!
O log: o caderno de anotações
Enquanto isso, na janela do servidor, cada visita deixou três linhas:
$ php -S localhost:8000 [Fri Oct 16 20:43:26 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 20:43:27 2026] 127.0.0.1:55434 Accepted « alguém se conectou [Fri Oct 16 20:43:27 2026] 127.0.0.1:55434 [200]: GET /ola.php « pediu /ola.php; resposta 200 [Fri Oct 16 20:43:27 2026] 127.0.0.1:55434 Closing « conexão encerrada
- 127.0.0.1
- Quem fez o pedido: o próprio computador (é o endereço numérico de
localhost). - :55434
- A porta do lado do visitante, sorteada a cada conexão. Serve para você saber que linhas pertencem à mesma visita.
- Accepted / Closing
- "Aceita" e "Fechando": o começo e o fim da conversa.
- [200]
- O código de status da resposta:
200= tudo certo;404= não encontrado. - GET /ola.php
- O que foi pedido: o método (
GET, "obter"; formulários usam tambémPOST) e o caminho.
O log é a primeira coisa a olhar quando uma página "não aparece": ele diz se o pedido chegou, qual caminho foi pedido e o que o servidor respondeu. No navegador, você vai notar pedidos que não fez, como GET /favicon.ico: o navegador procura sozinho o iconezinho da aba.
A página inicial e os endereços que não existem
Quando o endereço termina numa pasta, como http://localhost:8000/, o servidor procura nela um arquivo index.php ou index.html. E aqui há um comportamento do php -S que surpreende: se o arquivo pedido não existe e há um index.php na raiz, ele muitas vezes entrega o index.php no lugar, com código 200, em vez de um erro. Numa pasta sem index.php, o pedido inexistente dá 404 e o log mostra o motivo:
$ curl -i localhost:8000/naoexiste.php
HTTP/1.1 404 Not Found
Host: localhost:8001
Date: Fri, 16 Oct 2026 23:43:28 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: text/html; charset=UTF-8
Content-Length: 614
<!doctype html><html><head><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 Not Found</title><style>
body { background-color: #fcfcfc; color: #333333; margin: 0; padding:0; }
h1 { font-size: 1.5em; font-weight: normal; background-color: #9999cc; min-height:2em; line-height:2em; border-bottom: 1px inset black; margin: 0; }
h1, p { padding-left: 10px; }
code.url { background-color: #eeeeee; font-family:monospace; padding:0 2px;}
</style>
</head><body><h1>Not Found</h1><p>The requested resource <code class="url">/naoexiste.php</code> was not found on this server.</p></body></html>
(Esse teste foi feito com um segundo servidor, na porta 8001, por isso o Host.) No log do servidor:
$ php -S localhost:8001 [Fri Oct 16 20:43:27 2026] PHP 8.5.8 Development Server (http://localhost:8001) started [Fri Oct 16 20:43:28 2026] 127.0.0.1:44906 Accepted [Fri Oct 16 20:43:28 2026] 127.0.0.1:44906 [404]: GET /naoexiste.php - No such file or directory « "arquivo ou pasta inexistente" [Fri Oct 16 20:43:28 2026] 127.0.0.1:44906 Closing
No navegador, essa página aparece como um título "Not Found" numa faixa roxa. Se você quer controlar o que acontece com cada endereço, use um roteador.
Experimente uma página servida. Esta caixa faz o papel do php -S: ela abre o endereço /index.php e mostra o resultado.
Missão: a página mostra o caminho pedido, que fica em $_SERVER["REQUEST_URI"]. Acrescente um parágrafo que mostre o método do pedido, que fica em $_SERVER["REQUEST_METHOD"], no formato Método: GET.
<!DOCTYPE html> <html lang="pt-BR"> <head><meta charset="utf-8"><title>Teste</title></head> <body> <h1>Servidor da Ana</h1> <p>Você pediu: <?= htmlspecialchars($_SERVER["REQUEST_URI"]) ?></p> </body> </html>
<!DOCTYPE html> <html lang="pt-BR"> <head><meta charset="utf-8"><title>Teste</title></head> <body> <h1>Servidor da Ana</h1> <p>Você pediu: <?= htmlspecialchars($_SERVER["REQUEST_URI"]) ?></p> <p>Método: <?= htmlspecialchars($_SERVER["REQUEST_METHOD"]) ?></p> </body> </html>
O $_SERVER é uma lista que o PHP preenche com informações sobre o pedido; a aula $_SERVER e Outras Superglobais mostra o resto. O htmlspecialchars protege a página caso o endereço tenha caracteres especiais (aula Escapar a Saída).
Parando o servidor: Ctrl+C
Para desligar, vá à janela do servidor e aperte Ctrl+C (segure Ctrl e toque C). O terminal mostra ^C, o servidor para e o prompt volta (às vezes grudado no ^C):
$ php -S localhost:8000 -t public [Fri Oct 16 21:01:47 2026] PHP 8.5.8 Development Server (http://localhost:8000) started ^C $ echo $? 0 « o servidor terminou sem erro
Fechar a janela do terminal também desliga o servidor. Depois de parado, o navegador passa a mostrar "Não é possível acessar esse site" (ou "Conexão recusada"), e o curl diz Failed to connect to localhost port 8000.
O PHP lê o arquivo de novo a cada pedido. Mudou o código? Salve e recarregue a página (F5). Só é preciso parar e ligar de novo para trocar a pasta, a porta ou o roteador, ou depois de instalar uma extensão.
Escolhendo a pasta do site: -t
Sem nada mais, o servidor publica a pasta onde você estava quando o ligou. A opção -t (de target, "alvo", ou document root, a "raiz dos documentos") escolhe outra pasta. Isso é muito usado para deixar fora do alcance do navegador os arquivos que não devem ser públicos. O blog da Ana está organizado assim:
blog/
├── config.php « senha do banco: NÃO pode ser acessível
└── public/ « só isto vai para a internet
├── index.php
├── sobre.php
└── css/
└── estilo.css
<?php $titulo = "Blog da Ana"; ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title><?= $titulo ?></title> <link rel="stylesheet" href="/css/estilo.css"> </head> <body> <h1><?= $titulo ?></h1> <p>Você pediu: <?= htmlspecialchars($_SERVER["REQUEST_URI"]) ?></p> </body> </html>
De dentro da pasta blog, a Ana liga o servidor apontando para public:
$ cd blog $ php -S localhost:8000 -t public [Fri Oct 16 21:01:33 2026] PHP 8.5.8 Development Server (http://localhost:8000) started
E testa, em outra janela:
$ curl localhost:8000/
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Blog da Ana</title>
<link rel="stylesheet" href="/css/estilo.css">
</head>
<body>
<h1>Blog da Ana</h1>
<p>Você pediu: /</p> « o / levou ao index.php de public
</body>
</html>
$ curl localhost:8000/css/estilo.css
h1 { color: purple; } « arquivos comuns são entregues como estão
$ curl localhost:8000/config.php
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Blog da Ana</title>
<link rel="stylesheet" href="/css/estilo.css">
</head>
<body>
<h1>Blog da Ana</h1>
<p>Você pediu: /config.php</p> « o config.php não está em public: veio o index.php
O config.php ficou fora do alcance: para o servidor, a pasta public é o "mundo inteiro". Repare também no comportamento que você viu antes: como o arquivo pedido não existe dentro de public, o servidor entregou o index.php. O log confirma:
$ php -S localhost:8000 -t public [Fri Oct 16 21:01:33 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 21:01:33 2026] 127.0.0.1:54562 Accepted [Fri Oct 16 21:01:33 2026] 127.0.0.1:54562 [200]: GET / [Fri Oct 16 21:01:33 2026] 127.0.0.1:54562 Closing [Fri Oct 16 21:01:33 2026] 127.0.0.1:54578 Accepted [Fri Oct 16 21:01:33 2026] 127.0.0.1:54578 [200]: GET /css/estilo.css [Fri Oct 16 21:01:33 2026] 127.0.0.1:54578 Closing [Fri Oct 16 21:01:33 2026] 127.0.0.1:54600 Accepted [Fri Oct 16 21:01:33 2026] 127.0.0.1:54600 [200]: GET /config.php [Fri Oct 16 21:01:33 2026] 127.0.0.1:54600 Closing
Os frameworks como o Laravel e o Symfony usam exatamente essa organização: uma pasta public com o index.php, e todo o resto fora dela. Por isso o comando de teste deles é parecido com php -S localhost:8000 -t public.
Erros e avisos aparecem na página
No servidor embutido, os avisos do PHP vão para a resposta, em HTML (com <b> e <br />), para aparecerem no navegador. Este arquivo usa uma variável que não existe:
<?php echo "<p>Olá, $nome!</p>\n";
$ curl localhost:8000/sobre.php <br /> <b>Warning</b>: Undefined variable $nome in <b>/home/ana/blog/public/sobre.php</b> on line <b>2</b><br /> <p>Olá, !</p>
No navegador, isso aparece como uma linha "Warning: Undefined variable $nome in …/sobre.php on line 2" acima do texto. Num PHP instalado pelo Ubuntu, com o php.ini de produção, o aviso não aparece na página: vai para o terminal do servidor (veja Instalar no Linux, seção do php.ini). A aula Lendo Erros do PHP ensina a ler a mensagem.
O script roteador
Sites modernos têm endereços "bonitos": /contato, /produtos/42, sem .php no fim. Não existe um arquivo contato na pasta; um programa decide o que mostrar para cada endereço. Esse programa se chama roteador (router), como um porteiro que encaminha cada visitante.
No php -S, você passa o roteador depois do endereço. A partir daí, todo pedido passa primeiro por ele:
<?php
// router.php — o "porteiro": decide o que fazer com cada pedido
$caminho = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($caminho === "/") {
echo "<h1>Página inicial</h1>\n";
} elseif ($caminho === "/contato") {
echo "<h1>Fale com a Ana</h1>\n";
} elseif (file_exists(__DIR__ . $caminho)) {
return false; // arquivo de verdade (imagem, CSS): o servidor entrega como está
} else {
http_response_code(404);
echo "<h1>Página não encontrada</h1>\n";
}
Muita coisa aqui ainda é novidade (if, funções, return); cada uma tem a sua aula. A ideia geral, linha a linha:
| Linha | O que faz |
|---|---|
$caminho = parse_url(…, PHP_URL_PATH); | Pega só o caminho do endereço pedido, sem o ?x=1 do fim. |
if ($caminho === "/") | Se pediram a raiz, mostra a página inicial (aula if, elseif, else). |
elseif ($caminho === "/contato") | Se pediram /contato, mostra a página de contato. |
elseif (file_exists(__DIR__ . $caminho)) | Se existe um arquivo com esse nome na pasta… |
return false; | …devolve false: é o sinal combinado com o php -S para "entregue o arquivo como está". |
http_response_code(404); | Senão, responde com o código 404 e uma mensagem própria. |
$ cd rotas $ php -S localhost:8000 router.php [Fri Oct 16 21:02:03 2026] PHP 8.5.8 Development Server (http://localhost:8000) started
Os testes, em outra janela (no curl -i, as linhas Host, Date e Connection do cabeçalho foram omitidas):
$ curl localhost:8000/ <h1>Página inicial</h1> $ curl localhost:8000/contato <h1>Fale com a Ana</h1> « não existe arquivo "contato": o roteador respondeu $ curl localhost:8000/fotos/leia.txt texto de exemplo « arquivo de verdade: return false $ curl -i localhost:8000/blog HTTP/1.1 404 Not Found « o código que o roteador escolheu X-Powered-By: PHP/8.5.8 Content-type: text/html; charset=UTF-8 <h1>Página não encontrada</h1>
Um detalhe no log: os pedidos respondidos pelo próprio roteador só ganham as linhas Accepted e Closing; a linha com o código aparece só quando o servidor entrega um arquivo (o return false):
$ php -S localhost:8000 router.php [Fri Oct 16 21:02:03 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 21:02:03 2026] 127.0.0.1:33038 Accepted [Fri Oct 16 21:02:03 2026] 127.0.0.1:33038 Closing [Fri Oct 16 21:02:03 2026] 127.0.0.1:33054 Accepted [Fri Oct 16 21:02:03 2026] 127.0.0.1:33054 Closing [Fri Oct 16 21:02:03 2026] 127.0.0.1:33064 Accepted [Fri Oct 16 21:02:03 2026] 127.0.0.1:33064 [200]: GET /fotos/leia.txt [Fri Oct 16 21:02:03 2026] 127.0.0.1:33064 Closing [Fri Oct 16 21:02:03 2026] 127.0.0.1:33068 Accepted [Fri Oct 16 21:02:03 2026] 127.0.0.1:33068 Closing
Dá para combinar as duas opções: php -S localhost:8000 -t public public/index.php publica a pasta public e manda tudo passar pelo index.php. Os projetos do guia (Mini Blog, API REST) usam um roteador assim.
Outras formas de ligar
| Comando | O que faz | Quando usar |
|---|---|---|
php -S localhost:8000 | Publica a pasta atual, só para este computador. | O dia a dia. |
php -S localhost:8080 | Usa outra porta. | A 8000 está ocupada, ou você quer dois servidores ao mesmo tempo. |
php -S localhost:8000 -t public | Publica só a pasta public. | Projetos organizados, frameworks. |
php -S localhost:8000 router.php | Todo pedido passa pelo roteador. | Endereços "bonitos", APIs. |
php -S 0.0.0.0:8000 | Aceita pedidos de outros aparelhos da rede (o celular no mesmo Wi-Fi). | Testar no celular. Cuidado: qualquer um na mesma rede pode acessar enquanto estiver ligado. |
Com 0.0.0.0, a mensagem de início mostra esse endereço:
$ php -S 0.0.0.0:8000 [Fri Oct 16 21:02:15 2026] PHP 8.5.8 Development Server (http://0.0.0.0:8000) started
No celular, você não digita 0.0.0.0: digita o endereço do computador na rede local (algo como http://192.168.0.10:8000; o comando hostname -I mostra o seu no Linux).
Quando dá errado
$ php -S localhost:8000 [Fri Oct 16 21:02:03 2026] Failed to listen on localhost:8000 (reason: Address in use) $ echo $? 1
O que aconteceu: "Falha ao escutar em localhost:8000 (motivo: endereço em uso)". Outro programa já está usando essa porta, quase sempre um php -S que você esqueceu ligado em outra janela ou aba do terminal.
Como resolver: procure a janela do servidor antigo e pare-o com Ctrl+C, ou use outra porta: php -S localhost:8080. No Linux, ss -ltnp | grep 8000 mostra qual programa está ocupando a porta.
$ php -S localhost:80 [Fri Oct 16 21:01:50 2026] Failed to listen on localhost:80 (reason: Permission denied) $ echo $? 1
O que aconteceu: no Linux e no macOS, as portas abaixo de 1024 (como a 80, padrão dos sites) são reservadas ao administrador.
Como resolver: use uma porta alta, como 8000. Não use sudo para isso: rodar o servidor de testes como administrador é arriscado e desnecessário.
$ php -S localhost Invalid address: localhost
O que aconteceu: o -S exige endereço:porta. Sem os dois-pontos e o número, ele não sabe em que porta escutar.
Como resolver: php -S localhost:8000.
-t está errada
$ php -S localhost:8000 -t publico Directory publico does not exist.
O que aconteceu: a pasta passada ao -t não existe (aqui, um erro de digitação: publico em vez de public). O caminho é relativo à pasta onde você está.
Como resolver: confira com ls e digite o nome certo (o Tab completa nomes de pastas).
O que aconteceu: o servidor foi ligado na pasta errada. O caminho do endereço é sempre relativo à pasta do servidor (ou à do -t): ligado em ~, o arquivo ~/site/ola.php fica em /site/ola.php, não em /ola.php.
Como resolver: olhe o log: ele mostra o caminho pedido e o código. Pare o servidor, entre na pasta certa (cd site) e ligue de novo, ou use -t.
O que aconteceu: o servidor não está ligado (você apertou Ctrl+C ou fechou o terminal), ou a porta digitada é outra.
Como resolver: confira se a janela do servidor ainda mostra o started sem prompt depois, e se a porta da barra de endereços é a mesma do comando. Use http://, não https://: o servidor embutido não tem criptografia.
Por que não usar em produção
A própria documentação do PHP avisa: o servidor embutido é para desenvolvimento e testes. Ele atende um pedido de cada vez (se uma página demora, todas as outras esperam), não tem HTTPS, não tem as proteções e os ajustes de desempenho de um servidor de verdade e não foi feito para ficar exposto à internet.
Num site publicado, o trabalho é dividido entre dois programas:
| Peça | Exemplos | Papel |
|---|---|---|
| Servidor web | nginx, Apache, Caddy | Recebe milhares de conexões ao mesmo tempo, cuida do HTTPS, entrega imagens e CSS direto do disco e passa os pedidos .php adiante. |
| Executor do PHP | PHP-FPM (FastCGI Process Manager) | Mantém vários processos do PHP prontos, cada um atendendo um pedido; o servidor web conversa com ele pelo protocolo FastCGI. |
O caminho do pedido é o mesmo que você viu (navegador → servidor web → PHP → HTML de volta); só as peças são mais robustas. Nas hospedagens compartilhadas, tudo isso já vem configurado: você só envia os arquivos. Para o guia inteiro, o php -S basta.
Referência
| Comando ou mensagem | Significado |
|---|---|
php -S localhost:8000 | Liga o servidor na pasta atual. |
-t pasta | Escolhe a pasta raiz do site. |
php -S … router.php | Todo pedido passa pelo roteador; return false entrega o arquivo pedido como está. |
| Ctrl+C | Para o servidor. |
Development Server (…) started | Ligou; está esperando pedidos. |
[200]: GET /ola.php | Um pedido atendido com sucesso. |
[404]: GET /x.php - No such file or directory | Pedido de um arquivo que não existe. |
Failed to listen … (reason: Address in use) | Porta ocupada. |
Failed to listen … (reason: Permission denied) | Porta reservada (abaixo de 1024). |
Invalid address | Faltou :porta. |
Directory … does not exist. | Pasta do -t errada. |
Para fechar, monte um mini-roteador. Esta caixa abre o endereço /index.php/contato. Quando o caminho tem um pedaço depois do nome do arquivo PHP, o servidor chama o index.php e guarda esse pedaço extra (/contato) em $_SERVER["PATH_INFO"]. É outra forma de ter endereços "bonitos" sem um router.php. Por enquanto, o código responde "Página inicial" para qualquer caminho:
Missão: acrescente um elseif para o caminho "/contato" que mostre <h1>Fale com a Ana</h1>. Siga o modelo do router.php desta aula. Depois, troque o endereço na barra da caixa para localhost:8000/index.php/blog e aperte Enter para ver o 404.
<?php
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/") {
echo "<h1>Página inicial</h1>\n";
} else {
http_response_code(404);
echo "<h1>Página não encontrada: " . htmlspecialchars($caminho) . "</h1>\n";
}
<?php
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/") {
echo "<h1>Página inicial</h1>\n";
} elseif ($caminho === "/contato") {
echo "<h1>Fale com a Ana</h1>\n";
} else {
http_response_code(404);
echo "<h1>Página não encontrada: " . htmlspecialchars($caminho) . "</h1>\n";
}
O ?? "/" quer dizer "se não houver PATH_INFO (quando o endereço é só /index.php), use "/"" (aula ?? e ?->).
Exercícios
1. Ligar o servidor
Complete o comando que liga o servidor embutido em localhost, na porta 8000:
2. Só a pasta public
Complete para publicar só a pasta public:
3. O servidor respondeu Failed to listen on localhost:8000 (reason: Address in use). O que fazer?
4. No log aparece [404]: GET /contato.php - No such file or directory. O que isso quer dizer?
5. Num site de verdade, na internet, o que se usa no lugar do php -S?
Resumo
php -S localhost:8000liga um servidor web de testes na pasta atual; abrahttp://localhost:8000/arquivo.php.- O terminal fica ocupado com o log:
Accepted,[200]: GET /caminho,Closingpara cada pedido. - Ctrl+C para o servidor. Mudanças no código valem ao recarregar a página, sem reiniciar.
-t publicpublica só uma pasta; umrouter.phpdecide cada endereço (return false= entregar o arquivo).- Sem roteador, endereços inexistentes podem cair no
index.phpda raiz (com 200); semindex.php, dão 404. - Erros comuns:
Address in use(porta ocupada),Permission denied(porta abaixo de 1024),Invalid address(faltou a porta). - Só para desenvolvimento; em produção, nginx ou Apache com PHP-FPM.
- Próximo passo: Editores e IDEs.