
How to Set Up a Local WordPress Development Environment with wp-env?
Every WordPress developer has heard "it works on my machine." One teammate runs PHP 8.3 in a desktop app, another runs PHP 8.1 under MAMP, and the CI server runs whatever was installed two years ago. A plugin that passes every test locally throws a deprecation warning in production. The fix is a local environment that is defined in a file, committed with the code, and identical for everyone. For plugin and theme development, the simplest tool that does this is wp-env, the official WordPress development environment maintained by the Gutenberg project.
This article covers what wp-env is and when to use it, installing it, starting your first environment, configuring WordPress and PHP versions, plugins, themes, and wp-config.php constants with .wp-env.json, running WP-CLI, Composer, and PHPUnit inside the environment, debugging with Xdebug, managing the environment's lifecycle, and fixing the problems you are most likely to hit. The examples target wp-env 11.x and WordPress 7.1.
What Is wp-env?
wp-env is a Node.js command-line tool, published on npm as @wordpress/env, that creates a complete WordPress site in Docker containers with one command. Run it inside a plugin or theme folder and it:
- Downloads WordPress, PHP, and a MariaDB database into containers.
- Mounts your current folder into the site as a plugin or theme and activates it.
- Installs WordPress with a default admin account and pretty permalinks.
- Exposes the site at
http://localhost:8888.
Configuration lives in a .wp-env.json file at the root of your project, so every developer, and your CI pipeline, gets the same WordPress version, PHP version, plugins, and settings.
wp-env compared with other local tools
| Tool | Best for | Config in repo | Requires Docker |
|---|---|---|---|
| wp-env | Plugin and theme development, CI testing | Yes | Yes, by default |
| Docker Compose | Full control over every service | Yes | Yes |
| Local (desktop app) | Site builders who prefer a GUI | No | No |
| WordPress Playground | Instant throwaway sites in the browser | Blueprints | No |
wp-env focuses on code you are developing, not on hosting full client sites. If you need a custom web server, Redis, or a mail catcher, a hand-written Compose file gives you more control, as shown in how to run WordPress locally with Docker Compose. If you want a throwaway site with no installation at all, WordPress Playground runs entirely in the browser.
Prerequisites
wp-env relies on three common tools:
- Docker: Docker Desktop on macOS and Windows (with the WSL2 backend on Windows), or Docker Engine on Linux. Docker must be running before you start an environment.
- Node.js: the current LTS release. A version manager such as nvm makes switching versions easy.
- Git: used to download WordPress, plugins, and themes from repositories.
Check them from your terminal:
# Terminal
docker --version
node --version
git --version
Installing wp-env
You can install wp-env globally or per project.
Global install
# Terminal
npm -g i @wordpress/env
wp-env --version
Project install (recommended for teams)
Installing it as a dev dependency pins the version in package.json, so every developer and CI run uses the same release:
# Terminal
npm i @wordpress/env --save-dev
Then add scripts to package.json:
{
"scripts": {
"env": "wp-env",
"env:start": "wp-env start",
"env:stop": "wp-env stop",
"wp": "wp-env run cli wp"
},
"devDependencies": {
"@wordpress/env": "^11.17.0"
}
}
When you run wp-env through npm run, flags after the script name need an extra double dash so npm passes them through, for example npm run env:start -- --xdebug. You can also run the local binary directly with npm exec --no -- wp-env start.
Starting Your First Environment
Move into a plugin or theme folder and start the environment:
# Terminal
cd ~/projects/my-plugin
wp-env start
The first start takes a few minutes while Docker pulls images and WordPress downloads. When it finishes, open:
- Site:
http://localhost:8888 - Admin:
http://localhost:8888/wp-admin/ - Username:
admin - Password:
password
The database user is root with the password password. These credentials are only for local development and are never exposed outside your machine.
Because wp-env detected a plugin in the current folder, it mounted the folder into wp-content/plugins/my-plugin and activated it. Edit a PHP file, refresh the browser, and the change is live. There is no build or sync step for PHP.
To stop the environment and free the ports, while keeping all data:
# Terminal
wp-env stop
Where wp-env keeps its files
wp-env stores downloaded sources and generated files in a home directory: ~/.wp-env on macOS and Windows, and ~/wp-env on Linux. Each project gets its own subfolder named after a hash of the project path. Set the WP_ENV_HOME environment variable to move it.
Configuring wp-env with .wp-env.json
Without a config file, wp-env uses sensible defaults: the latest WordPress release, the default PHP version, and the current folder as a plugin or theme. A .wp-env.json file at the project root makes the setup explicit and shareable. Here is a realistic configuration for a plugin, saved as .wp-env.json:
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": null,
"phpVersion": "8.3",
"plugins": [".", "https://downloads.wordpress.org/plugin/query-monitor.zip"],
"themes": ["https://downloads.wordpress.org/theme/twentytwentyfive.zip"],
"port": 8888,
"config": {
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"WP_ENVIRONMENT_TYPE": "local"
},
"mappings": {
"wp-content/mu-plugins": "./dev/mu-plugins"
},
"phpmyadmin": true,
"lifecycleScripts": {
"afterStart": "wp-env run cli wp theme activate twentytwentyfive"
}
}
The $schema key gives you autocomplete and validation in editors such as VS Code.
The most useful options
| Option | Default | What it controls |
|---|---|---|
core | null | WordPress source; null means the latest production release |
phpVersion | null | PHP version, such as "8.1" or "8.3" |
mariadbVersion | null (LTS) | Database version: "lts", "latest", or a version like "10.11" |
plugins | [] | Plugins to install and activate |
themes | [] | Themes to install |
port | 8888 | Web server port |
autoPort | false | Find the next free port automatically if the configured one is busy |
config | See below | wp-config.php constants |
mappings | {} | Extra local folders mounted anywhere in the WordPress install |
multisite | false | Installs a multisite network |
phpmyadmin | false | Enables phpMyAdmin for browsing the database |
lifecycleScripts | {} | Commands to run after start, reset, cleanup, or destroy |
Sources: paths, GitHub, and ZIP files
The core, plugins, themes, and mappings fields accept several kinds of source:
- Relative or absolute paths:
".","../my-theme","~/projects/shared-plugin" - GitHub repositories:
"WordPress/gutenberg#trunk", optionally with a subfolder path and a branch or tag - SSH repositories:
"ssh://git@github.com/owner/repo.git#main" - ZIP files:
"https://downloads.wordpress.org/plugin/query-monitor.zip"
To pin a specific WordPress release, point core at its ZIP, for example "https://wordpress.org/wordpress-6.8.zip". That is how you test that a plugin still works on the oldest WordPress version you support.
Default wp-config values
On the development environment, wp-env defines WP_DEBUG and SCRIPT_DEBUG as true, along with a few testing constants and the site URLs. Anything in config overrides those defaults, and setting a constant to null prevents it from being defined at all. URL constants include your configured port automatically.
Activating plugins without activating everything
Every entry in plugins is activated. To make a plugin available without activating it, mount it with mappings instead:
{
"plugins": ["."],
"mappings": {
"wp-content/plugins/my-test-helper": "./tests/helper-plugin"
}
}
Personal overrides with .wp-env.override.json
Sometimes one developer needs a different port or an extra debugging plugin. Instead of editing the shared file, create .wp-env.override.json next to it and add it to .gitignore. Its values take precedence:
{
"port": 8890,
"config": {
"SAVEQUERIES": true
}
}
Only config and mappings are merged with the base file. Other keys, such as plugins, replace the base value entirely, so list every plugin you need if you override that key.
Applying configuration changes
wp-env does not reinstall sources on every start. When you change .wp-env.json in ways that need new downloads or reapplied settings, run:
# Terminal
wp-env start --update
This downloads updates to remote sources and reapplies configuration without deleting your content.
Running WP-CLI, Composer, and PHPUnit
The environment includes a cli container with WP-CLI, Composer, and PHPUnit. Run any command inside it with wp-env run:
# Terminal
wp-env run cli wp plugin list
wp-env run cli wp user create editor editor@example.com --role=editor --user_pass=password
wp-env run cli wp rewrite structure /%postname%/
wp-env run cli wp post generate --count=25
wp-env run cli wp shell
The container names you can target are cli, wordpress, mysql, composer, and phpmyadmin. When a command includes flags that wp-env itself understands, separate them with a double dash so they reach the container, for example wp-env run cli php -- --help.
By default, commands run from the WordPress root. Use --env-cwd to run them inside your plugin folder, which is essential for Composer and PHPUnit:
# Terminal
wp-env run cli --env-cwd=wp-content/plugins/my-plugin composer install
wp-env run cli --env-cwd=wp-content/plugins/my-plugin vendor/bin/phpunit
Add these as npm scripts so nobody has to remember the paths:
{
"scripts": {
"composer": "wp-env run cli --env-cwd=wp-content/plugins/my-plugin composer",
"test:php": "wp-env run cli --env-cwd=wp-content/plugins/my-plugin vendor/bin/phpunit"
}
}
If you are new to WP-CLI, how to use WP-CLI to manage WordPress from the command line covers the commands you will use most.
Importing an existing database
Because your project folder is mounted inside the container, you can import a SQL dump stored in the project:
# Terminal
wp-env run cli --env-cwd=wp-content/plugins/my-plugin wp db import data/dev-snapshot.sql
wp-env run cli wp search-replace 'https://example.com' 'http://localhost:8888' --skip-columns=guid
Never commit production dumps that contain customer data. Use sanitized snapshots for development.
Debugging with Xdebug
Xdebug is installed in the environment but turned off by default, because it slows PHP down. Enable it when you start:
# Terminal
wp-env start --xdebug
wp-env start --xdebug=profile,trace,debug
Without a value, --xdebug sets the debug mode for step debugging. Running wp-env start again without the flag turns Xdebug off.
To connect VS Code, install the PHP Debug extension and add a launch configuration that maps the container path to your local folder. Save it as .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug (wp-env)",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html/wp-content/plugins/my-plugin": "${workspaceFolder}"
}
}
]
}
Start listening in VS Code, set a breakpoint, and load the page. For lighter-weight debugging, WP_DEBUG_LOG writes notices and warnings to wp-content/debug.log, and the Query Monitor plugin from the example configuration shows queries, hooks, and HTTP requests for each page.
Managing the Environment Lifecycle
wp-env has a small set of lifecycle commands. Know the difference between them, because some permanently delete data:
| Command | What it does | Deletes content |
|---|---|---|
wp-env start | Creates or starts the environment | No |
wp-env stop | Stops containers and frees ports | No |
wp-env status | Shows whether it is running, its URL, ports, and config path | No |
wp-env logs | Streams PHP and Docker logs | No |
wp-env reset | Resets the WordPress database | Yes, database |
wp-env cleanup | Removes containers, volumes, networks, and local files; keeps images | Yes, everything |
wp-env destroy | Removes containers, volumes, networks, images, and local files | Yes, everything |
cleanup and destroy ask for confirmation unless you pass --force. Use cleanup for a fresh start that restarts quickly, because Docker images are kept. Use destroy when you also want to reclaim the disk space used by images.
Running parallel environments
Each config file gets its own isolated containers and data. To run a second environment from the same folder, for example to test against an older WordPress version, create another config file and point wp-env at it:
# Terminal
wp-env start
WP_ENV_PORT=8890 wp-env start --config=.wp-env.legacy.json
wp-env status --config=.wp-env.legacy.json
wp-env stop --config=.wp-env.legacy.json
This replaces the older built-in tests environment, which is deprecated in recent wp-env releases.
The Experimental Playground Runtime
wp-env also supports an experimental runtime powered by WordPress Playground, which runs WordPress in WebAssembly with an SQLite database instead of Docker:
# Terminal
wp-env start --runtime=playground
It is useful on machines where Docker is not available, but it lacks some features, most notably the wp-env run command. wp-env remembers the chosen runtime until you destroy the environment. For day-to-day development, the Docker runtime remains the complete option.
Using wp-env with Block Development
wp-env pairs naturally with @wordpress/scripts. A typical block plugin runs both side by side: wp-env serves WordPress, and the build tool watches and compiles JavaScript:
# Terminal
npx @wordpress/create-block@latest my-block
cd my-block
wp-env start
npm start
@wordpress/create-block scaffolds the plugin, wp-env start mounts and activates it, and npm start rebuilds the block on every change. The full block workflow is covered in how to build a custom Gutenberg block with block.json and React.
Using wp-env in CI
Because the environment is defined in a file, the same setup runs in continuous integration. A minimal GitHub Actions job looks like this:
# .github/workflows/tests.yml
name: PHP tests
on: [push, pull_request]
jobs:
phpunit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npx wp-env start
- run: npm run composer -- install
- run: npm run test:php
GitHub-hosted Ubuntu runners include Docker, so no extra setup is needed. Automatic port selection is disabled when the CI environment variable is set, which keeps ports predictable in pipelines.
Common Problems and Fixes
- "Could not connect to Docker." Docker is not running. Start Docker Desktop or the Docker service and try again.
- Port 8888 is already in use. Another environment or app holds the port. Run
wp-env stopin other projects, set a differentport, exportWP_ENV_PORT, or start with--auto-port. - Config changes have no effect. wp-env does not reapply configuration on a normal start. Run
wp-env start --update. - The site behaves strangely after many experiments. Reset the database with
wp-env reset, then start again. If that does not help,wp-env cleanuprecreates everything from scratch. - Flags passed through npm are ignored. Add an extra double dash, for example
npm run env:start -- --xdebug, so npm forwards the flag. - MariaDB fails to start after changing
mariadbVersionto an older release. A database created by a newer version cannot be opened by an older one. Runwp-env cleanup, then start again. - Composer or PHPUnit cannot find files. The command ran in the WordPress root. Add
--env-cwd=wp-content/plugins/your-plugin.
wp-env FAQ
No. It works from a plugin folder, a theme folder, a full WordPress installation, or any folder with a wp-env.json file. It is designed around developing code, so it is less suited to hosting complete client sites with custom server software.
The admin username is admin and the password is password. The site runs at localhost on port 8888 unless you change the port. The database user is root with the password password. These defaults are for local development only.
No. Stopping only shuts down the containers. Your posts, settings, and uploads remain and come back on the next start. Reset deletes the database, while cleanup and destroy remove the entire environment.
Set phpVersion in wp-env.json and point core at the ZIP of the WordPress release you want, then run start with the update flag. To keep your main environment, put those settings in a separate config file and start it with the config option on a different port.
There is an experimental Playground runtime that uses WebAssembly and SQLite instead of Docker. It covers quick testing but does not support every feature, including the run command for WP-CLI and other tools.
Conclusion
wp-env gives you a reproducible WordPress environment with almost no setup: install Docker and Node.js, run wp-env start in your plugin or theme folder, and log in. A committed .wp-env.json pins the WordPress version, PHP version, plugins, themes, and constants for everyone on the team, while .wp-env.override.json handles personal tweaks without touching the shared file.
From there, wp-env run gives you WP-CLI, Composer, and PHPUnit inside the environment, --xdebug enables step debugging when you need it, and separate config files let you test against older versions in parallel. Learn the difference between stop, reset, cleanup, and destroy, and the same file that runs on your laptop will run in CI.
Here are some useful references for going deeper on wp-env:
- Block Editor Handbook: @wordpress/env package reference — every command, option, and configuration field.
- Block Editor Handbook: Get started with wp-env — the official quick-start guide.
- npm: @wordpress/env — release history and installation details.
- Docker Docs: Get Docker — installing Docker Desktop or Docker Engine.
- Xdebug: Xdebug settings: mode — what each Xdebug mode does.


