start here

Built-In Web Server

PHP includes a small development web server. It lets you run a local PHP site without installing Nginx, Apache, Caddy, or PHP-FPM.

Use it for learning, demos, small local projects, and quick HTTP checks. Do not use it as a production server.

Starting The Server

The basic command is:

php -S 127.0.0.1:8000

That starts a local server on http://127.0.0.1:8000. Press Ctrl+C in the terminal to stop it.

Most projects should use a public document root:

php -S 127.0.0.1:8000 -t public

The -t public option means the server should serve files from the public directory. That keeps source files, configuration, and vendor code outside the web root.

A Minimal public/index.php

Create public/index.php:

PHP example
<?php

declare(strict_types=1);

header('Content-Type: text/plain');

echo 'Hello from PHP' . PHP_EOL;
echo 'SAPI: ' . PHP_SAPI . PHP_EOL;

// Browser output:
// Hello from PHP
// SAPI: cli-server

Start the server:

php -S 127.0.0.1:8000 -t public

Then open:

http://127.0.0.1:8000/

PHP_SAPI reports cli-server because the built-in server runs from the CLI runtime.

Checking Request Information

The built-in server gives you normal web request values in $_SERVER.

PHP example
<?php

declare(strict_types=1);

header('Content-Type: application/json');

echo json_encode([
    'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
    'uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
    'query' => $_SERVER['QUERY_STRING'] ?? '',
    'sapi' => PHP_SAPI,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
echo PHP_EOL;

// Example URL:
// http://127.0.0.1:8000/?page=2
//
// Prints:
// {
//     "method": "GET",
//     "uri": "/?page=2",
//     "query": "page=2",
//     "sapi": "cli-server"
// }

This is useful when learning how HTTP requests reach PHP.

Static Files

The built-in server serves static files directly when they exist under the document root. For example:

public/style.css
public/images/logo.png

Those files can be requested as:

http://127.0.0.1:8000/style.css
http://127.0.0.1:8000/images/logo.png

If a file does not exist, the server may route the request to a PHP script depending on how you started it.

Router Scripts

A router script gives you more control. It can let existing static files pass through and send everything else to index.php.

Create router.php in the project root:

PHP example
<?php

declare(strict_types=1);

$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$file = __DIR__ . '/public' . $path;

if (is_string($path) && $path !== '/' && is_file($file)) {
    return false;
}

require __DIR__ . '/public/index.php';

Start the server with the router:

php -S 127.0.0.1:8000 router.php

Returning false tells the built-in server to serve the requested static file itself. Otherwise the router includes the front controller.

Binding Address

Use 127.0.0.1 for local-only development:

php -S 127.0.0.1:8000 -t public

Binding to 0.0.0.0 listens on all network interfaces:

php -S 0.0.0.0:8000 -t public

Only use 0.0.0.0 when you deliberately want another device or container to reach the server. Do not expose the built-in server to the public internet.

Limitations

The built-in server is not a production replacement for Nginx, Apache, Caddy, or a managed runtime.

Important limitations:

  • it is designed for development
  • it does not replace proper TLS, static file, caching, compression, or process management setup
  • it runs as the current user
  • it uses the CLI PHP configuration
  • it does not prove PHP-FPM is configured correctly

If production uses PHP-FPM behind Nginx, a local built-in server can help you develop routes, but it cannot verify the production web server configuration.

What You Should Be Able To Do

After this lesson, you should be able to start the built-in server, choose a document root, inspect request information, use a router script, and explain why the built-in server is for development only.

For junior PHP work, this matters because the built-in server is the fastest way to see PHP through HTTP while learning. The professional habit is knowing where its usefulness ends.

Practice

Practice: Start A Local PHP Server

Create a tiny public/index.php file and run it with PHP's built-in server.

Task

Build an index.php that:

  • sets Content-Type: text/plain
  • prints a greeting
  • prints PHP_SAPI
  • prints the request URI

Start the server with -t public and visit it in the browser.

Use strict types. Keep the expected browser output inside the PHP code block as comments.

Afterward, add a short note explaining why public should be the document root.

Show solution

This public/index.php proves the request is going through the built-in server.

PHP example
<?php

declare(strict_types=1);

header('Content-Type: text/plain');

echo 'Hello from the built-in server' . PHP_EOL;
echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'URI: ' . ($_SERVER['REQUEST_URI'] ?? '/') . PHP_EOL;

// Browser output for http://127.0.0.1:8000/hello?page=1:
// Hello from the built-in server
// SAPI: cli-server
// URI: /hello?page=1

Start it with:

php -S 127.0.0.1:8000 -t public

public should be the document root so configuration, source files, private storage, and dependencies are not directly web-accessible.

Task: Check Request Info

Create a small JSON endpoint that reports request information from the built-in server.

Requirements

Build an endpoint that prints:

  • request method
  • request URI
  • query string
  • PHP_SAPI

Use strict types and json_encode() with JSON_THROW_ON_ERROR. Keep example output inside the PHP code block as comments.

Afterward, add a short note explaining why request data belongs in $_SERVER for this example, not $argv.

Show solution

This endpoint returns a small JSON summary of the incoming HTTP request.

PHP example
<?php

declare(strict_types=1);

header('Content-Type: application/json');

echo json_encode([
    'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
    'uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
    'query' => $_SERVER['QUERY_STRING'] ?? '',
    'sapi' => PHP_SAPI,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
echo PHP_EOL;

// Example URL:
// http://127.0.0.1:8000/?page=2
//
// Prints:
// {
//     "method": "GET",
//     "uri": "/?page=2",
//     "query": "page=2",
//     "sapi": "cli-server"
// }

Request data belongs in $_SERVER here because the code is handling an HTTP request. $argv is for command-line arguments in CLI scripts.

Task: Router Script

Create a router script for the built-in server.

Requirements

Build a router.php that:

  • checks the requested path
  • lets existing files under public be served by the built-in server
  • sends all other requests to public/index.php

Use strict types. Include the command used to start the server.

Afterward, add a short note explaining what return false means in a built-in server router script.

Show solution

This router serves real static files and sends application routes to the front controller.

PHP example
<?php

declare(strict_types=1);

$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$file = __DIR__ . '/public' . $path;

if (is_string($path) && $path !== '/' && is_file($file)) {
    return false;
}

require __DIR__ . '/public/index.php';

Start the server with:

php -S 127.0.0.1:8000 router.php

In a built-in server router script, return false means "let the server handle this request normally". That is useful for static files such as CSS, JavaScript, and images.