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
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:
rootpoints to the project'spublicdirectory- static files are served directly
- missing routes fall back to
index.php - FastCGI points at the correct FPM socket or TCP listener
SCRIPT_FILENAMEresolves 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
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
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.