start here

Local Server Setup

The common mistake is treating every local server as equivalent. PHP's built-in server, Apache, Nginx, Caddy, and PHP-FPM all run PHP through different paths.

Start With The Document Root

Most modern PHP applications use a public document root:

project/
  app/
  config/
  public/
    index.php
  vendor/

The web server should expose public, not the whole project. That prevents users from directly requesting source files, private configuration, dependency code, or storage files.

Built-In PHP Server

The fastest local option is PHP's built-in server:

php -S 127.0.0.1:8000 -t public

It is good for learning, quick demos, and simple local development.

Minimal public/index.php:

PHP example
<?php

declare(strict_types=1);

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

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

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

The built-in server uses CLI PHP, so it does not prove PHP-FPM or a real web server is configured correctly.

Apache

Apache can run PHP in different ways:

  • through PHP-FPM using FastCGI
  • through an Apache PHP module, where available
  • through a packaged local stack such as XAMPP, MAMP, WampServer, or Laragon

The important local checks are:

  • document root points to public
  • rewrite rules send application routes to index.php
  • PHP version matches the project
  • errors go to a known log
  • configuration does not expose private project files

Apache-style projects often use .htaccess for rewrites when the server allows it. Production servers may disable .htaccess, so know whether your local setup matches production.

Nginx With PHP-FPM

Nginx does not execute PHP itself. It passes PHP requests to PHP-FPM.

The request path is:

browser -> Nginx -> PHP-FPM -> public/index.php

The key local checks are:

  • root points to the project's public directory
  • static files are served directly
  • missing routes fall back to index.php
  • FastCGI points at the correct FPM socket or TCP listener
  • SCRIPT_FILENAME resolves to the correct file

Many "PHP downloads instead of runs" or 502 Bad Gateway errors are server/FPM handoff problems, not application bugs.

Caddy With PHP-FPM

Caddy can be a concise local web server and reverse proxy. It can pass PHP requests to PHP-FPM with php_fastcgi.

A local Caddy-style setup still needs the same checks:

  • site root is public
  • PHP-FPM is running
  • the PHP-FPM socket or listener is correct
  • logs are visible
  • the runtime version matches the project

The syntax is different from Nginx, but the architecture is similar: Caddy handles HTTP and PHP-FPM executes PHP.

Verifying The Web Runtime

Add a temporary diagnostic endpoint in a safe local environment:

PHP example
<?php

declare(strict_types=1);

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

echo 'PHP: ' . PHP_VERSION . PHP_EOL;
echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Loaded ini: ' . (php_ini_loaded_file() ?: 'none') . PHP_EOL;
echo 'Document root: ' . ($_SERVER['DOCUMENT_ROOT'] ?? 'unknown') . PHP_EOL;
echo 'Script filename: ' . ($_SERVER['SCRIPT_FILENAME'] ?? 'unknown') . PHP_EOL;

Use this locally to confirm which runtime handled the request. Remove it or protect it before production.

Logs

Local server debugging depends on logs.

Places to check:

  • application logs
  • web server access logs
  • web server error logs
  • PHP-FPM logs
  • PHP-FPM slow logs
  • terminal output from the built-in server

If the browser shows a blank page or generic error, the next useful answer is usually in a log.

Choosing A Local Setup

Use the built-in server when you are learning or checking application routes quickly.

Use Apache, Nginx, Caddy, Docker, or a local development app when you need behaviour closer to production: rewrites, TLS, multiple sites, FPM pools, real static file handling, or service integration.

Use containers when the team wants every developer and CI to run the same versions of PHP, extensions, database, cache, and queue services.

What You Should Be Able To Do

After this lesson, you should be able to explain the difference between the built-in server and web-server-plus-FPM setups, choose public as the document root, identify how requests reach index.php, and verify which PHP runtime handled a local web request.

For junior PHP work, this matters because local server setup can hide or create bugs. A developer who can trace the request path can debug setup issues without blaming application code too early.

Practice

Practice: Verify A Local Server Setup

Create a local server verification note for a PHP project.

Task

Include:

  • the chosen local server option
  • the document root
  • how requests reach public/index.php
  • how static files are served
  • where logs appear
  • how to prove which PHP runtime handled the request

Add a small diagnostic PHP endpoint that prints PHP_VERSION, PHP_SAPI, loaded php.ini, DOCUMENT_ROOT, and SCRIPT_FILENAME.

Use strict types. Add a short note explaining why the diagnostic endpoint should not be left public in production.

Show solution
Server option: built-in PHP server for local development
Document root: public
Request path: browser -> cli-server -> public/index.php
Static files: served directly from public
Logs: terminal running php -S
Start command: php -S 127.0.0.1:8000 -t public

Diagnostic endpoint:

PHP example
<?php

declare(strict_types=1);

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

echo 'PHP: ' . PHP_VERSION . PHP_EOL;
echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Loaded ini: ' . (php_ini_loaded_file() ?: 'none') . PHP_EOL;
echo 'Document root: ' . ($_SERVER['DOCUMENT_ROOT'] ?? 'unknown') . PHP_EOL;
echo 'Script filename: ' . ($_SERVER['SCRIPT_FILENAME'] ?? 'unknown') . PHP_EOL;

// Example built-in server output:
// PHP: 8.5.6
// SAPI: cli-server
// Loaded ini: /etc/php.ini
// Document root: /path/to/project/public
// Script filename: /path/to/project/public/index.php

Do not leave this endpoint public in production because it exposes runtime paths, configuration details, and version information that attackers do not need.