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.

Você vai aprender
  • Ligar o servidor com php -S localhost:8000 e 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 -s minú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ém POST) 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.

Não precisa reiniciar a cada mudança

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:

LinhaO 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

ComandoO que fazQuando usar
php -S localhost:8000Publica a pasta atual, só para este computador.O dia a dia.
php -S localhost:8080Usa outra porta.A 8000 está ocupada, ou você quer dois servidores ao mesmo tempo.
php -S localhost:8000 -t publicPublica só a pasta public.Projetos organizados, frameworks.
php -S localhost:8000 router.phpTodo pedido passa pelo roteador.Endereços "bonitos", APIs.
php -S 0.0.0.0:8000Aceita 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

"Address in use": a porta já está ocupada
$ 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.

"Permission denied" na porta 80
$ 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.

"Invalid address": faltou a porta
$ 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.

"Directory … does not exist": a pasta do -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).

404 em todas as páginas, ou a página errada

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.

"Não é possível acessar esse site" no navegador

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çaExemplosPapel
Servidor webnginx, Apache, CaddyRecebe 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 PHPPHP-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 mensagemSignificado
php -S localhost:8000Liga o servidor na pasta atual.
-t pastaEscolhe a pasta raiz do site.
php -S … router.phpTodo pedido passa pelo roteador; return false entrega o arquivo pedido como está.
Ctrl+CPara o servidor.
Development Server (…) startedLigou; está esperando pedidos.
[200]: GET /ola.phpUm pedido atendido com sucesso.
[404]: GET /x.php - No such file or directoryPedido 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 addressFaltou :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:

php

2. Só a pasta public

Complete para publicar só a pasta public:

php -S localhost:8000

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:8000 liga um servidor web de testes na pasta atual; abra http://localhost:8000/arquivo.php.
  • O terminal fica ocupado com o log: Accepted, [200]: GET /caminho, Closing para cada pedido.
  • Ctrl+C para o servidor. Mudanças no código valem ao recarregar a página, sem reiniciar.
  • -t public publica só uma pasta; um router.php decide cada endereço (return false = entregar o arquivo).
  • Sem roteador, endereços inexistentes podem cair no index.php da raiz (com 200); sem index.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.