wp-cli/server-command
WP-CLI Server Command adds the wp server command, letting you launch PHP's built-in web server to run WordPress locally without Apache or Nginx. Just run wp server and get a working dev site, with options for host, port, docroot, and router. Ideal for quick local development and testing.
wp-cli/server-command provides the wp server command. It launches PHP's built-in web server (php -S) against a WordPress document root and ships with a router that makes pretty permalinks work. It is a local-development tool, not a production web server.
Minimal setup:
# Confirm WP-CLI and the command are available
wp --info
wp help server
# Run from the WordPress root (the directory containing index.php and wp-config.php)
cd /path/to/wordpress
wp server
Then open http://localhost:8080. Stop the server with Ctrl+C.
If wp server is not recognized:
wp package install wp-cli/server-command
# or update WP-CLI, since the command is commonly bundled
wp cli update
Where to look first:
wp help server for the exact options supported by your installed version.router.php to understand how static files and WordPress permalinks are handled.First real use case: skip MAMP/Local/Valet for a quick plugin or theme check. From a checked-out WordPress site, run wp server, activate your plugin with wp plugin activate my-plugin, and iterate in the browser. Because it runs before WordPress is fully bootstrapped, startup is fast, but the HTTP requests themselves still load WordPress normally.
Basic daily loop
wp server --host=127.0.0.1 --port=8080
Useful options:
| Option | Purpose |
|---|---|
--host=<host> |
Bind address. Default localhost. Use 0.0.0.0 for containers or LAN/device testing. |
--port=<port> |
Listen port. Default 8080. |
--docroot=<path> |
Directory to serve. Default is the current working directory. |
--router=<file> |
Custom router script. Default is the package's bundled router. |
Run from a subdirectory or Bedrock-style layout
--docroot must point at the directory containing index.php, not necessarily the project root:
# WordPress installed under public/
wp server --docroot=public
# Bedrock-style layout
wp server --docroot=web
If WP-CLI itself cannot locate WordPress from your current directory, combine the global --path flag with the server's --docroot:
wp --path=public server --docroot=public
--path tells WP-CLI where WordPress lives; --docroot tells the web server what to serve. They are separate concerns.
Keep local URLs in sync
The most common source of "why is it redirecting to production?" is stale home and siteurl values:
wp db export backup.sql
wp option update home 'http://localhost:8080'
wp option update siteurl 'http://localhost:8080'
# Or migrate all URLs, including serialized data
wp search-replace 'https://example.com' 'http://localhost:8080' --all-tables --precise
For per-developer setups, define constants in wp-config.php instead:
define( 'WP_HOME', 'http://localhost:8080' );
define( 'WP_SITEURL', 'http://localhost:8080' );
Also disable HTTPS-only admin behavior locally if it causes redirect loops:
define( 'FORCE_SSL_ADMIN', false );
Custom router for auth, CORS, or logging
The router is the main extension point. It runs on every request. Return false to let PHP's built-in server serve an existing file directly; otherwise include WordPress's front controller.
<?php
// router.php
$root = rtrim( $_SERVER['DOCUMENT_ROOT'], '/\\' );
$path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
// Let php -S serve real static files.
if ( is_file( $root . $path ) ) {
return false;
}
// Optional: basic auth for a shared dev environment.
if ( empty( $_SERVER['PHP_AUTH_USER'] ) ) {
header( 'WWW-Authenticate: Basic realm="Dev"' );
http_response_code( 401 );
exit;
}
// Optional: request logging.
file_put_contents(
sys_get_temp_dir() . '/wp-server.log',
$_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . PHP_EOL,
FILE_APPEND
);
require $root . '/index.php';
Run it with:
wp server --router="$(pwd)/router.php"
Use an absolute path in scripts; relative router paths can be resolved from the current working directory.
Project scripts
Composer:
{
"scripts": {
"serve": "wp server --host=127.0.0.1 --port=8080"
}
}
composer run serve
package.json:
{
"scripts": {
"wp:serve": "wp server --host=127.0.0.1 --port=8080"
}
}
Per-project defaults with wp-cli.yml
WP-CLI supports command-specific defaults, which keeps the invocation short:
server:
host: 127.0.0.1
port: 8080
docroot: web
Then wp server picks those defaults up in that project.
Docker and container workflows
Bind to all interfaces inside the container and publish the port:
wp server --host=0.0.0.0 --port=8080
services:
wordpress:
image: wordpress:cli
command: wp server --host=0.0.0.0 --port=8080
ports:
- "8080:8080"
Verify the container user can write to wp-content/uploads; otherwise media uploads will fail even though the site loads.
Multisite
Subdirectory multisite generally works with the bundled router. Subdomain multisite needs hostnames such as site1.localhost:8080 to resolve and reach the server. Bind to 0.0.0.0, add host entries or wildcard DNS, and let WordPress route by HTTP_HOST.
Never expose wp server in production. PHP's built-in server has no TLS, no .htaccess/mod_rewrite, no process manager, and minimal concurrency control. Treat it as a disposable local server.
It is single-threaded by default. Any WordPress request that makes a loopback HTTP request to itself—WP Cron spawns, Site Health loopback tests, some REST integrations—can hang until timeout. On PHP 7.4+ (Unix-like systems), spawn workers:
PHP_CLI_SERVER_WORKERS=4 wp server
Also consider disabling WP Cron locally and running it manually:
wp config set DISABLE_WP_CRON true --raw
wp cron event run --due-now
No HTTPS. Browsers treat http://localhost as a secure context, so most development works, but OAuth providers and secure-cookie testing may still complain. Put a TLS-terminating reverse proxy or tunnel in front when needed.
Port conflicts are common. PHP reports Address already in use. Find the offender and switch ports:
lsof -iTCP:8080 -sTCP:LISTEN
wp server --port=8081
If you change the port, update home/siteurl or your WP_HOME/WP_SITEURL constants to match.
localhost can bind IPv6 only on some systems. If the browser cannot connect but the process is running, try --host=127.0.0.1 explicitly.
--host=0.0.0.0 exposes the server to your network. There is no authentication by default. Use a custom router with basic auth or restrict access via firewall.
--path is not --docroot. Running wp server from a plugin directory usually serves the wrong directory and may not find WordPress. Run from the site root, or pass both flags.
The bundled router fixes pretty permalinks; a broken custom router can mask missing assets. If your router always includes index.php, requests for missing CSS/JS return HTML with a 200 instead of a 404. Return false only for real files, and let the server handle the rest.
return false only works in the top-level router script. If you factor router logic into an included file, the return value will not tell php -S to serve the file directly.
Code changes are picked up per request, but config changes are not. Editing PHP files does not require a restart. Changing php.ini, the PHP binary, or environment variables does. Restart with Ctrl+C and rerun.
Check which PHP binary is running. wp --info shows WP-CLI's PHP. If the server behaves differently from your web server, confirm extensions and versions. WP_CLI_PHP can override the binary WP-CLI uses.
File permissions still matter. The server runs as your shell user. If WordPress files are owned by www-data, uploads and plugin updates may fail. Fix ownership or run the server as the appropriate user.
Page-caching plugins can hide changes. Deactivate caching plugins during active development, or purge their caches after edits.
Redirect loops often come from stale URLs or forced SSL. Check home, siteurl, FORCE_SSL_ADMIN, and any WP_HOME/WP_SITEURL constants before debugging the server itself.
Log to a file when the terminal scrolls too fast.
wp server 2>&1 | tee /tmp/wp-server.log
Debug WordPress errors through the terminal first. The built-in server prints PHP fatals and warnings to stderr. Also enable WP_DEBUG and WP_DEBUG_LOG for errors that occur after WordPress boots.
Custom routers are the best extension point. Use them for request logging, basic auth, CORS headers, SPA fallbacks, or special handling for /wp-json/. Do not use them as a substitute for production rewrite rules.
Windows support is limited. PHP_CLI_SERVER_WORKERS is not supported there, and path/quoting behavior can differ. Prefer WSL for parity with Unix-like development environments.
How can I help you explore Laravel packages today?