Local Development Environments
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), @wordpress/env (wp-env) 12.0.0, which needs Node.js 22.13 or later, the Playground CLI
@wp-playground/cli 3.1.61, WP-CLI 2.12.0 and the official Docker images
wordpress:7.1.3-php8.4-apache, wordpress:cli-2.12.0-php8.4 and mariadb:11.8. WordPress needs PHP 7.4 at least
and PHP 8.3 or later is recommended, so every environment below runs PHP 8.4. WordPress 7.2 is in beta and planned for
2026-12-08. It was checked on 2026-10-28.
A local environment is a complete WordPress site (PHP, a web server, a database and the WordPress files) running on a
developer’s machine. Theme, plugin and block developers write and test code in it; site owners and administrators try
updates, plugins and imports in it without touching the live site. This page runs the Lumen Press project (the
lumen-catalog plugin, the lumen block theme and a must-use plugin) in three ways and then shows how the local copy
fits with staging and production.
Local, staging and production
A professional WordPress project has at least three copies of the site, each with its own database, URL and
WP_ENVIRONMENT_TYPE. Code moves up the chain through version control; content moves down as database exports. A
lower environment’s database is never pushed over production, which keeps receiving comments, orders, users and edits
that the copies do not have.
wp-env, Compose or Playground"] -- "commit and push code" --> B["Git repository"] B --> C["CI: lint, tests, build"] C -- "deploy build" --> D["Staging
noindex, mail suppressed"] D -- "approved release" --> E["Production
lumenpress.example"] E -. "database export, sanitised" .-> D D -. "database and uploads copy" .-> A
Figure: code (solid arrows) moves from the local machine to production through Git, CI and staging; content (dotted arrows) is copied down from production.
-
Local (
local): writing code, running tests, trying plugins and updates;WP_DEBUGandSCRIPT_DEBUGon, errors displayed, no real mail. -
Development (
development, optional): a shared integration server; this value also turnsWP_DEBUGon when it is not defined. -
Staging (
staging): a recent copy of production on the production stack where releases are approved; errors logged but not displayed,noindex, password or IP protection, mail suppressed. -
Production (
production, the default):WP_DEBUG_DISPLAYoff, real mail, caches and backups on.
Choosing a tool
The Block Editor Handbook recommends wp-env, maintained by the WordPress project, and the Playground CLI for quick tests. The Advanced Administration Handbook lists many more options. The common ones:
-
wp-env (
@wordpress/env): needs Node.js 22.13+ and Docker, runs MariaDB. Best for plugin, theme and block development; ships the WordPress PHPUnit test files; used by Gutenberg and by Lumen Press. -
Docker Compose with the official images: needs Docker. Full control of PHP, web server and database (MariaDB or MySQL); closest to a containerised production stack.
-
Playground CLI (
@wp-playground/cli): needs Node.js 20.18+, no Docker, uses SQLite. Very fast throwaway sites, bug reproduction, Blueprint-driven demos, CI smoke tests. -
Playground in the browser: needs nothing but a browser. Trying a plugin, sharing a reproducible set-up as a Blueprint, teaching.
-
Desktop applications and other tools: WordPress Studio, Local, MAMP, XAMPP, DDEV, Lando, VVV (see Desktop applications).
The npm package @wp-now/wp-now is deprecated; its npm entry points to the Playground CLI, which has an equivalent for
every wp-now start option (see The Playground CLI).
All the set-ups below run the same lumen-press repository: lumen-catalog/ (the plugin), themes/lumen/ (the
block theme), mu-plugins/lumen-environment.php, content/ (import fixtures), and the committed configuration files
.wp-env.json, compose.yaml, .env.example, blueprint.json and package.json. .wp-env.override.json and .env
are ignored by Git.
wp-env
wp-env is a Node.js command that writes a Docker Compose configuration and runs it: a wordpress container (Apache
and PHP), a cli container (WP-CLI, Composer and PHPUnit) and a mysql container running MariaDB. It mounts the
project into the install and activates it. Generated files live in a per-project directory under ~/.wp-env
(~/wp-env on Linux), or under WP_ENV_HOME. Install it as a pinned development dependency, so every developer and
CI run the same version:
npm install --save-dev --save-exact @wordpress/env@12.0.0 # adds "@wordpress/env": "12.0.0" to package.json
npx wp-env --version
Flags passed through npm run need an extra double dash (npm run env:start -- --xdebug), otherwise npm keeps
them. npx wp-env <command> runs the local copy directly.
The .wp-env.json file
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": "https://wordpress.org/wordpress-7.1.3.zip",
"phpVersion": "8.4",
"mariadbVersion": "11.8",
"testsEnvironment": false,
"port": 8888,
"plugins": [ "./lumen-catalog" ],
"themes": [ "./themes/lumen" ],
"mappings": {
"wp-content/mu-plugins": "./mu-plugins",
"wp-content/uploads/lumen-fixtures": "./content"
},
"config": {
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": true
},
"lifecycleScripts": {
"afterStart": "npx wp-env run cli wp theme activate lumen && npx wp-env run cli wp rewrite structure /%postname%/ --hard"
}
}
-
core:null(latest release), a GitHub reference (WordPress/WordPress#master), a local path such as../wordpress-develop/build, or a ZIP URL. Pinning a release ZIP keeps every machine on the same version. The examples ran on 7.1.3; since 2026-10-28 production runs the 7.1.4 security release, so the pin is moved tohttps://wordpress.org/wordpress-7.1.4.zipto keep local and production on the same version. -
phpVersion,mariadbVersion: the latter acceptslts(the default),latestor a number. MariaDB cannot start on data written by a newer version; lowering it requireswp-env cleanupfirst. -
pluginsare installed and activated;themesare only installed, hence theafterStartactivation. Local paths, GitHub and SSH repositories and ZIP URLs are accepted. -
mappings: mounts at any path without activation (must-use plugins, fixtures, a test plugin, an.htaccesswithphp_valuelimits). -
config:wp-config.phpconstants merged with the defaults;nullremoves a default. -
port(8888, overridden byWP_ENV_PORT),mysqlPort(a random host port unless set;wp-env statusshows it),autoPort,phpmyadmin,phpmyadminPort,multisite. -
lifecycleScripts: host commands run afterstart,reset,cleanupanddestroy. They run on every start, so they must be idempotent. -
testsEnvironment,testsPortandenvare deprecated. Without"testsEnvironment": false, wp-env 12 still starts a second site on port 8889 and prints a warning; a second configuration file with--configreplaces it.
wp-env defines WP_DEBUG and SCRIPT_DEBUG (true), WP_ENVIRONMENT_TYPE (local), FS_METHOD (direct, so
plugins install without FTP credentials), WP_PHP_BINARY, the WP_TESTS_ constants, and WP_SITEURL/WP_HOME set
to http://localhost:<port>. The administrator is admin/password and the database user root/password;
these well-known credentials are acceptable only because the site listens on localhost.
.wp-env.override.json, ignored by Git, wins over .wp-env.json. Only config and mappings are merged; any other
key replaces the whole value, so a personal plugins list must repeat ./lumen-catalog.
wp-env start --config=.wp-env.test.json starts an isolated environment (own containers and database) from a second
file, with its override .wp-env.test.override.json; every command meant for it needs the same --config.
Everyday commands
npx wp-env start # create or restart the site on http://localhost:8888
npx wp-env start --update # re-download sources, re-apply config (content is kept)
npx wp-env start --xdebug # Xdebug "debug" mode on port 9003; --xdebug=profile,trace also works
npx wp-env status # runtime, URL, ports; --json for scripts
npx wp-env run cli wp option get home # WP-CLI in the cli container
npx wp-env run cli wp plugin list --status=active
npx wp-env logs # PHP and container logs
npx wp-env stop # stop containers, free the ports
npx wp-env reset # empty the development database
npx wp-env cleanup --force # remove containers, volumes and files, keep images
npx wp-env destroy --force # remove everything, including images
wp-env run targets cli, wordpress, mysql, composer or phpmyadmin and starts in the WordPress root
(--env-cwd=wp-content/plugins/lumen-catalog changes that, for example for composer install). It
treats options it knows as its own: wp-env run cli php -- --help is needed to pass --help to PHP. In wp-env 12
the database reset command is wp-env reset; wp-env clean still works but prints a
deprecation warning, and the "Get started with wp-env" handbook page still shows clean all.
--spx enables the SPX profiler. The Lumen Catalog import command runs the same way (wp-env run cli wp lumen import, see WP-CLI).
wp-env 12 also has an experimental Playground runtime (wp-env start --runtime=playground): no Docker, SQLite, Xdebug
and Multisite supported, but no wp-env run, SPX or MariaDB version. The runtime chosen at the first start is kept
until wp-env destroy.
Common problems: a busy port 8888 (stop the other project, set WP_ENV_PORT, or use --auto-port); Docker not
running (wp-env reports a Docker Compose error); configuration changes not applied (wp-env start --update); a broken
install (wp-env reset, and as a last resort wp-env destroy, which deletes all local content); "no space left on
device" while pulling images (the Docker disk is full); MariaDB 5.5 on Apple silicon, which exists only for amd64 and
needs DOCKER_DEFAULT_PLATFORM=linux/amd64, running every container under emulation.
Docker Compose with the official images
A hand-written Compose file is the better choice when the project needs a specific web server, PHP-FPM, MySQL instead
of MariaDB, or the same images as a containerised production. The official wordpress image has apache (default),
fpm and fpm-alpine variants for PHP 8.2 to 8.5, and a cli variant with WP-CLI on Alpine. On first start, the
entrypoint copies WordPress into an empty /var/www/html and writes a wp-config.php that reads the
WORDPRESS_* variables through getenv(). The database must exist; the mariadb image creates it from
MARIADB_DATABASE.
name: lumen-press
services:
db:
image: mariadb:11.8
environment:
MARIADB_DATABASE: lumen
MARIADB_USER: lumen
MARIADB_PASSWORD: ${LUMEN_DB_PASSWORD:?Set LUMEN_DB_PASSWORD in .env}
MARIADB_RANDOM_ROOT_PASSWORD: "1"
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 20
wordpress:
image: wordpress:7.1.3-php8.4-apache
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:80"
environment: &wp-env
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: lumen
WORDPRESS_DB_USER: lumen
WORDPRESS_DB_PASSWORD: ${LUMEN_DB_PASSWORD}
WORDPRESS_TABLE_PREFIX: lp_
WORDPRESS_DEBUG: "1"
WORDPRESS_CONFIG_EXTRA: |
define( 'WP_ENVIRONMENT_TYPE', 'local' );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', true );
volumes: &wp-volumes
- wp-core:/var/www/html
- ./lumen-catalog:/var/www/html/wp-content/plugins/lumen-catalog
- ./themes/lumen:/var/www/html/wp-content/themes/lumen
- ./mu-plugins:/var/www/html/wp-content/mu-plugins
cli:
image: wordpress:cli-2.12.0-php8.4
profiles: ["tools"]
user: "33:33"
depends_on:
db:
condition: service_healthy
environment:
<<: *wp-env
HOME: /tmp
volumes: *wp-volumes
volumes:
db-data:
wp-core:
The password comes from a .env file next to compose.yaml that is never committed; the repository carries only
.env.example with LUMEN_DB_PASSWORD=<choose-a-local-password>. Starting the stack and installing WordPress:
cp .env.example .env # then set a local-only password
docker compose up -d
docker compose run --rm cli wp core install \
--url=http://localhost:8080 --title="Lumen Press" \
--admin_user=admin --admin_email=admin@lumenpress.test --skip-email
docker compose run --rm cli wp plugin activate lumen-catalog
docker compose run --rm cli wp theme activate lumen
docker compose run --rm cli wp rewrite structure '/%postname%/'
docker compose run --rm cli wp eval 'echo wp_get_environment_type(), PHP_EOL;'
docker compose down # stop; add -v to delete the database and core volumes
Source: WP-CLI Commands: wp core install
Without --admin_password, wp core install generates a random password and prints it once. The choices in the file
avoid these errors:
-
Health check:
depends_onwithcondition: service_healthywaits for MariaDB; otherwise the first requests show "Error establishing a database connection". -
Port bound to 127.0.0.1:
"8080:80"alone publishes a site that displays errors on every network interface. -
User 33:33 for WP-CLI: Debian-based
wordpressimages run Apache aswww-datawith UID 33, Alpinecliimages use UID 82. Withoutuser: "33:33", files WP-CLI creates are not writable by Apache.HOME=/tmpgives WP-CLI a writable cache. -
Shared environment: the
clicontainer reads the samewp-config.php, so it needs the sameWORDPRESS_*variables (the YAML anchor&wp-env). -
WORDPRESS_CONFIG_EXTRA: PHP evaluated insidewp-config.php, the place forWP_ENVIRONMENT_TYPE. Keys and salts default to random values; every supported variable has a_FILEform that reads a Docker secret. -
Core volume: changing the image tag does not update core inside an existing
wp-corevolume; runwp core updateor recreate the volume. -
Table prefix:
lp_matches the Lumen Press production prefix, so imported dumps need no renaming (see Database Schema).
Docker itself is covered in Compose for Local Development; production containers in Containers, Docker and Kubernetes.
WordPress Playground
Playground runs WordPress entirely in WebAssembly: PHP compiled to WASM and SQLite instead of MySQL, with no server.
https://playground.wordpress.net/ starts a site in seconds in the browser. It keeps up to five autosaves in browser
storage, which are recovery points, not backups; a temporary Playground (?storage=temp) is lost when the tab is
closed or refreshed. The WebAssembly files (15 to 30 MB) are downloaded first, large plugins take 30 to 60 seconds to
load, and not every WP-CLI command is supported. It suits demos, bug reproduction and teaching rather than daily work
on a large site.
A Blueprint is a JSON file describing a Playground site: versions, landing page, login and a list of steps. The same file drives the browser and the Playground CLI. Lumen Press keeps one in the repository:
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"landingPage": "/wp-admin/edit.php?post_type=lp_book",
"preferredVersions": { "php": "8.4", "wp": "7.1" },
"login": true,
"steps": [
{ "step": "activatePlugin", "pluginPath": "lumen-catalog/lumen-catalog.php" },
{ "step": "setSiteOptions", "options": { "blogname": "Lumen Press (Playground)" } },
{ "step": "wp-cli", "command": "wp post create --post_type=lp_book --post_title='The Lantern Keeper' --post_status=publish" }
]
}
The Playground CLI
@wp-playground/cli serves a Playground site on a local port. start detects the project type, persists the site in
~/.wordpress-playground/sites/<hash of the directory>/ and opens a browser. server gives full control; it keeps
files and the SQLite database in a temporary directory unless wp-content is mounted.
# Simple mode: run from the plugin directory; the site is kept between runs
cd lumen-catalog && npx @wp-playground/cli@3.1.61 start --php=8.4
# Advanced mode: mount the plugin and apply the Blueprint on port 9400
npx @wp-playground/cli@3.1.61 server \
--port=9400 --php=8.4 --wp=7.1 \
--mount=./lumen-catalog:/wordpress/wp-content/plugins/lumen-catalog \
--blueprint=./blueprint.json
run-blueprint applies a Blueprint without a web server and build-snapshot produces a ZIP of the result.
--mount-before-install mounts before installation, for example a full WordPress checkout at /wordpress/. The CLI
logs every visitor in as the administrator, so it is for localhost only. Migrating from wp-now:
npx @wp-now/wp-now start becomes npx @wp-playground/cli@latest start, and --wp, --php, --blueprint,
--skip-browser and --reset keep their meaning. Only the storage changes: wp-now start --path=./plugin stored the
site for that path, while start stores it for the directory it runs from, so cd ./plugin first.
Desktop applications
WordPress Studio, an open source application from Automattic for macOS, Windows and Linux, creates local sites, starts them from Blueprints and can sync with WordPress.com and Pressable sites. Local (LocalWP, from WP Engine) creates sites with a chosen web server, PHP and MySQL version. MAMP and XAMPP install a general Apache, PHP and MySQL stack on which WordPress is installed by hand; DDEV and Lando are Docker-based project tools; VVV is a Vagrant set-up. None is endorsed here. What matters is the same for every tool: PHP and database versions that match production, a configuration reproducible from files in the repository, and a site that is not reachable from the network. A purely graphical set-up is hard to reproduce on another machine or in CI, which is why Lumen Press commits its configuration files.
Staging copies
A staging site is a copy of production on the same hosting stack (PHP build, database server, web server, caches, CDN
rules), used to approve a release. Managed hosts often create one with a click; on a self-managed server it is built
from a database export and a copy of wp-content/uploads:
# On production
wp db export lumen-prod.sql --add-drop-table
# On staging (WP_ENVIRONMENT_TYPE is 'staging' in its wp-config.php)
wp db import lumen-prod.sql
wp search-replace 'https://lumenpress.example' 'https://staging.lumenpress.example' \
--all-tables-with-prefix --skip-columns=guid --report-changed-only
wp cache flush
wp rewrite flush --hard
wp search-replace handles PHP-serialized values (options, widget settings), which a plain SQL REPLACE() corrupts
because serialized strings store their length. The GUID column must never change. Run with --dry-run first and add
--precise for complex serialized data. The same steps pull a copy into wp-env, with http://localhost:8888 as the
target URL. Copying production data to a laptop is a transfer of personal data: delete or anonymise customers and
subscribers first and keep dumps encrypted. Uploads and Multisite are covered in
Importing, Exporting and Migrating.
A staging site must sit behind HTTP authentication or an IP allow list, must not be indexed, must not mail real users (password resets, order notifications, newsletters), and its cron jobs must not act on external services as production would, such as the Lumen Catalog distributor stock sync.
WP_ENVIRONMENT_TYPE
WordPress 5.5 added wp_get_environment_type(). It returns local, development, staging or production, read
from the WP_ENVIRONMENT_TYPE environment variable or the constant of the same name; the constant wins. A missing or
unknown value returns production, and the filters for custom types were removed in 5.5.1. With development and no
WP_DEBUG defined, WordPress defines WP_DEBUG as true. The result is cached on the first call, so set the value in
wp-config.php or the server environment, never later in a plugin. Hosts are asked to set staging on staging copies.
// wp-config.php on the Lumen Press staging server
define( 'WP_ENVIRONMENT_TYPE', 'staging' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/var/log/wordpress/lumen-staging-debug.log' );
define( 'WP_DEBUG_DISPLAY', false );
The Lumen Press must-use plugin, mapped into every environment above, keeps non-production copies out of search engines and stops them sending mail:
<?php
/**
* Plugin Name: Lumen Environment Guard
* Description: Keeps non-production copies of the Lumen Press site private and quiet.
*/
defined( 'ABSPATH' ) || exit;
if ( 'production' !== wp_get_environment_type() ) {
// Never let search engines index local, development or staging copies.
add_filter( 'wp_robots', 'wp_robots_no_robots' );
// Do not send real email from copies of the site; log it instead. A local copy that
// relays to Mailpit sets LUMEN_SMTP_HOST, so its mail goes through; staging never sends.
$lumen_smtp_host = defined( 'LUMEN_SMTP_HOST' ) ? (string) LUMEN_SMTP_HOST : (string) getenv( 'LUMEN_SMTP_HOST' );
if ( '' === $lumen_smtp_host || 'staging' === wp_get_environment_type() ) {
add_filter(
'pre_wp_mail',
static function ( $short_circuit, $atts ) {
error_log( sprintf( '[lumen] mail to %s suppressed: %s', implode( ', ', (array) $atts['to'] ), $atts['subject'] ) );
return true;
},
10,
2
);
}
}
pre_wp_mail (5.7) short-circuits wp_mail() when a callback returns a non-null value; true reports the mail as
sent. The filter is skipped on a local or development copy whose LUMEN_SMTP_HOST points at a mail catcher such as
Mailpit, as set up in
Email Delivery, while staging never
sends mail. wp_robots_no_robots() (5.7) adds noindex to the robots meta tag. Because a missing or misspelt value
means production, code must never read "not configured" as "test".
Pitfalls and corrections to older books
-
A local site is not a staging site. One older book calls its desktop install a "local staging" environment. Staging runs on the production stack; a laptop runs another web server, PHP build and database and cannot reveal problems with OPcache, the object cache, permissions or the proxy.
-
Official tooling is missing from the older books. They name MAMP, XAMPP or Docker in general terms and build on a desktop application; none mentions wp-env, Playground, Blueprints or
WP_ENVIRONMENT_TYPE, all available when they were published. -
Plain permalinks are not a shortcut for development or staging. One older book suggests them because SEO does not matter there. Permalinks that differ from production hide rewrite bugs; keep the production structure (wp-env enables pretty permalinks by default) and rely on
noindex. -
Password protection alone is not enough for staging: set
WP_ENVIRONMENT_TYPEtostagingso code can disable mail, payments and external syncs. -
Never push a lower environment’s database to production, and never deploy wp-env or Playground files; deploy code (see Deployment and CI/CD).