PHP Built-in Web Server

La Lakshmi Priya Updated 17 Oct 2026
12 min read ·Lesson 15 of 22

What Is the Built-in Web Server?

In simple terms: PHP includes a small web server that you can start with one command. It lets you open your PHP files in a browser without installing Apache or Nginx.

Technically: The built-in web server is a feature of the PHP command-line interface (CLI), available since PHP 5.4. It listens for HTTP requests on a chosen address and port, executes PHP files for those requests, and serves static files such as CSS, JavaScript, and images.

Why It Exists

A browser cannot run PHP. Something must receive the HTTP request, pass the .php file to the PHP engine, and send back the result. Normally that is a full web server such as Nginx with PHP-FPM — powerful, but more setup than you need while learning or building locally. The built-in server removes that setup so you can focus on writing code.

When to Use It

  • Learning PHP and running tutorial examples
  • Local development of websites, APIs, and front controllers
  • Quick demos and testing

When Not to Use It

  • Never on a production (live) server. It is designed for development only: it has no HTTPS, very limited performance tuning, and has not been hardened against attacks the way Nginx or Apache have.
  • When you need features of a real server, such as .htaccess rewrite rules, virtual hosts, or HTTPS. Those require Apache, Nginx, or a similar server.

Starting the Server

Syntax

php -S <address>:<port> [-t <document-root>] [router-script]
PartMeaning
-SStart the built-in server. The capital S is required.
<address>The network address to listen on, usually localhost
<port>The port number, for example 8000
-t <document-root>Optional. The folder that URLs map to. Defaults to the current directory.
router-scriptOptional. A PHP file that runs for every request (explained later).

Simple Example

Create a folder with a file named index.php:

<?php
echo "<h1>The server is working</h1>";
echo "<p>Current time: " . date('H:i:s') . "</p>";

Open a terminal in that folder and run:

php -S localhost:8000

The terminal shows a message similar to:

[Thu Oct  8 10:15:02 2026] PHP 8.5.1 Development Server (http://localhost:8000) started

Open http://localhost:8000 in your browser.

Output

The server is working
Current time: 10:15:07

How It Works

  1. The server listens on port 8000 of localhost (your own computer).
  2. The URL http://localhost:8000 requests / — the root of the site.
  3. When a URL points to a directory, the server looks for index.php, then index.html, and serves the first one it finds.
  4. index.php is executed, and its output is sent to the browser.
  5. Each request is logged in the terminal:
[Thu Oct  8 10:15:07 2026] [::1]:52344 Accepted
[Thu Oct  8 10:15:07 2026] [::1]:52344 [200]: GET /
[Thu Oct  8 10:15:07 2026] [::1]:52344 Closing

The [200] is the HTTP status code (200 means success). These logs are useful while debugging: you can see every request, including requests for missing files that return [404].

The server keeps running until you stop it with Ctrl + C in the terminal.

Note: The terminal also shows PHP warnings and errors that occur while handling requests, so keep it visible while you work.


How URLs Map to Files

Without a router script, the server maps URLs directly to files inside the document root.

Given this folder:

my-site/
├── index.php
├── about.php
├── css/
│   └── style.css
└── blog/
    └── index.php

and the server started inside my-site:

URLWhat the Server Does
http://localhost:8000/Runs index.php
http://localhost:8000/about.phpRuns about.php
http://localhost:8000/css/style.cssSends style.css as a static file with the text/css content type
http://localhost:8000/blog/Runs blog/index.php
http://localhost:8000/contact.phpReturns 404 Not Found because the file does not exist

Fallback Behavior for Missing Paths

When a URL does not match a file, the server looks for index.php or index.html in the requested directory. If neither exists, it continues searching in the parent directories up to the document root. If an index file is found, it is executed and the remaining part of the URL is available in $_SERVER['PATH_INFO']. If no index file is found, the response is 404.

This behavior is convenient but can be surprising: a request for /blog/some/unknown/path may be answered by blog/index.php. If you see an unexpected page instead of a 404, this fallback is usually the reason.


Choosing the Document Root With -t

Why a Separate Public Folder Is Used

In real projects, not every file should be reachable through a URL. Configuration files, database credentials, and application code should be outside the folder that the web server exposes. A typical structure is:

my-app/
├── config/
│   └── database.php     ← must NOT be accessible by URL
├── src/
│   └── helpers.php      ← application code
└── public/
    ├── index.php        ← the only entry point
    └── css/
        └── style.css

Start the server with public as the document root:

php -S localhost:8000 -t public

What Changes

  • http://localhost:8000/ runs public/index.php.
  • http://localhost:8000/css/style.css serves public/css/style.css.
  • There is no URL that reaches config/database.php, because it is outside the document root.

public/index.php can still load application code with paths relative to its own location:

<?php
require __DIR__ . '/../src/helpers.php';

__DIR__ is the directory containing the current file (my-app/public), so /../src/helpers.php points to my-app/src/helpers.php. This works because PHP code can read any file the operating system allows — only URLs are limited to the document root.

Practicing this structure with the built-in server prepares you for production, where Nginx or Apache is configured with the same public folder as its document root. Frameworks such as Laravel and Symfony use exactly this layout.


Using a Router Script

What It Is and Why It Is Needed

Without a router, every URL must match a real file. Modern applications usually prefer clean URLs such as /products/42 instead of /product.php?id=42. On Apache or Nginx, this is done with rewrite rules. The built-in server does not support rewrite rules, but it supports a router script instead.

A router script is a PHP file that runs at the start of every request. It decides how to handle the request.

The rule that controls its behavior:

  • If the router script returns false, the server handles the request normally — for example, it serves the requested static file.
  • If the router script produces output (or returns anything else), that output is the response.

Basic Router Example

<?php
// router.php

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($path === '/') {
    echo '<h1>Home</h1>';
} elseif ($path === '/about') {
    echo '<h1>About Us</h1>';
} elseif ($path === '/contact') {
    echo '<h1>Contact</h1>';
} else {
    http_response_code(404);
    echo '<h1>404 - Page Not Found</h1>';
}

Start the server with the router:

php -S localhost:8000 router.php

Output

URLPage ShowsStatus Code
/Home200
/aboutAbout Us200
/contactContact200
/pricing404 - Page Not Found404

How the Router Works

  • $_SERVER['REQUEST_URI'] contains the requested path and query string, for example /about?ref=footer.
  • parse_url(..., PHP_URL_PATH) extracts only the path (/about), so query strings do not break the comparisons.
  • The if / elseif chain chooses what to output for each path.
  • http_response_code(404) sets the HTTP status code. Without it, the "not found" page would be sent with status 200, which misleads browsers and search engines into treating it as a real page.

This router has a problem, though: a request for /css/style.css also falls into the else branch and returns a 404, because the router handles every request, including static files. The next example fixes that.


Practical Example: Front Controller With Static Files

Most PHP applications use a front controller — a single index.php that receives all requests and decides what to do. The following setup works with the public folder structure shown earlier.

public/index.php

<?php
// Let the built-in server deliver existing static files directly
if (PHP_SAPI === 'cli-server') {
    $requestedFile = __DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

    if (is_file($requestedFile)) {
        return false;
    }
}

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

$pages = [
    '/'        => 'Home',
    '/about'   => 'About Us',
    '/contact' => 'Contact',
];

if (isset($pages[$path])) {
    $title = $pages[$path];
} else {
    http_response_code(404);
    $title = 'Page Not Found';
}
?>
<!DOCTYPE html>
<html>
<head>
    <title><?= htmlspecialchars($title) ?></title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <h1><?= htmlspecialchars($title) ?></h1>
</body>
</html>

Start the server, using public as the document root and public/index.php as the router:

php -S localhost:8000 -t public public/index.php

Step-by-Step Explanation

  1. PHP_SAPI === 'cli-server' — PHP_SAPI is a constant that identifies how PHP is running. Its value is cli-server only under the built-in server. Wrapping the static-file check in this condition means the same index.php works unchanged on Nginx or Apache later, where the web server delivers static files itself.
  2. __DIR__ . parse_url(...) — builds the full filesystem path of the requested URL inside public. For /css/style.css, this becomes .../public/css/style.css.
  3. is_file($requestedFile) — checks whether that path is an existing file (not a directory).
  4. return false; — tells the built-in server: "I am not handling this; serve the file normally." The stylesheet is sent with the correct content type.
  5. $pages array — maps paths to page titles. Using an array instead of a long if chain makes adding pages a one-line change.
  6. isset($pages[$path]) — checks whether the path exists as a key in the array.
  7. htmlspecialchars($title) — escapes special HTML characters before output. Here the titles are fixed, but escaping every dynamic value is a habit that prevents cross-site scripting (XSS) when values later come from users or databases.
  8. <?= ... ?> — short echo tag, equivalent to <?php echo ... ?>.

Expected Behavior

  • http://localhost:8000/about → "About Us" page, styled with style.css
  • http://localhost:8000/css/style.css → the raw CSS file
  • http://localhost:8000/unknown → "Page Not Found" with status 404

This is the same pattern frameworks use: one entry point, clean URLs, and static assets served directly.


Useful Variations

Use a Different Port

php -S localhost:8080

Use another port when 8000 is already taken by another program.

Pass Configuration Values

The -d option sets a php.ini value for this server only:

php -d display_errors=1 -d error_reporting=E_ALL -S localhost:8000

This is useful when you want to enable error display temporarily without editing php.ini.

Use a Specific php.ini File

php -c ./php-dev.ini -S localhost:8000

-c makes PHP load the given configuration file instead of the default one.

Access the Server From Other Devices

php -S 0.0.0.0:8000

0.0.0.0 means "listen on all network interfaces". Other devices on the same network — such as your phone — can then open http://<your-computer-ip>:8000 to test the site on a real mobile screen.

Warning: Only use 0.0.0.0 on a trusted network. Anyone who can reach your computer on that port can access the site, including any debug pages or test data.

Handle Several Requests at Once

By default, the built-in server handles one request at a time. On Linux and macOS, you can start multiple worker processes:

PHP_CLI_SERVER_WORKERS=4 php -S localhost:8000

This feature is available since PHP 7.4, is marked experimental, and is not supported on Windows.


Common Mistakes

Using a Lowercase -s

php -s localhost:8000

What happens: PHP does not start a server. Lowercase -s is a different option (it outputs syntax-highlighted source).

Fix: Use the capital -S.

"Address already in use"

[Thu Oct  8 10:20:00 2026] Failed to listen on localhost:8000 (reason: Address already in use)

Why it happens: Another program — or another PHP server you forgot to stop — is using the port.

Fix: Stop the other process (Ctrl + C in its terminal) or choose another port such as 8001.

Starting the Server in the Wrong Folder

What happens: Every page returns 404 Not Found.

Why it happens: Without -t, the document root is the directory where you ran the command. If you started the server in the parent folder, URLs no longer match your files.

Fix: Run the command inside the project folder, or specify the folder with -t.

Page Hangs When It Calls Its Own Server

What happens: A script uses file_get_contents('http://localhost:8000/api.php') or cURL to call an endpoint on the same server, and the page never finishes loading.

Why it happens: The single-process server is busy handling the first request, so it cannot answer the second one. Each request waits for the other — a deadlock.

Fix: Use PHP_CLI_SERVER_WORKERS (Linux/macOS), run the API on a second server instance with a different port, or call the code directly instead of over HTTP.

Expecting .htaccess Rules to Work

What happens: Clean URLs that work on Apache return 404 locally.

Why it happens: .htaccess is an Apache feature. The built-in server ignores it.

Fix: Use a router script, as shown above.

Using It in Production

Why it happens: It is easy to start, so it may seem like a quick way to host a site.

Why it is a problem: It lacks HTTPS, process management, request limits, and the security hardening of production servers. Under load, it becomes slow or unresponsive.

Fix: Use Nginx or Apache with PHP-FPM for any live site. See the PHP-FPM lesson.


Best Practices

  • Mirror production structure locally. Use a public folder with -t public so that file exposure works the same way locally and on the live server.
  • Use one front controller. Route all requests through public/index.php and keep the cli-server static-file check so the same file works everywhere.
  • Always set correct status codes. Use http_response_code() for 404 and other errors, so browsers, APIs, and search engines receive accurate responses.
  • Keep the terminal visible. Request logs and PHP errors appear there first.
  • Save the start command. In larger projects, store it in a script or in composer.json so every developer starts the server the same way:
{
    "scripts": {
        "serve": "php -S localhost:8000 -t public public/index.php"
    }
}

Then run composer serve. Composer scripts are covered in the Composer lesson. Note that Composer scripts have a default timeout of 300 seconds; for a long-running server, either add "process-timeout": 0 under "config" in composer.json or prefix the command with Composer0Config::disableProcessTimeout in the script list.

Official reference: https://www.php.net/manual/en/features.commandline.webserver.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
Ask about this post
AI Ask about this post

Ask questions about PHP Built-in Web Server and get answers drawn from it.

Signed-in readers only.