MAMP and WAMP served their era well, but they share a fundamental flaw: the PHP version, MySQL configuration, and server settings on your laptop have nothing to do with what runs in production. A containerized local environment packages the application, its runtime, and its configuration into a single unit — one that is identical everywhere it runs. This is what eliminates the “works on my machine” failure mode and what makes your local environment a reliable preview of staging and prod.
Why Containers Beat Traditional Local Servers
Three properties make containerized environments the right default for WordPress development today:
- Environmental parity. The PHP version, MySQL configuration, and server software defined in your
docker-compose.ymlor.lando.ymlare exactly what runs on every machine that clones the project — yours, a teammate’s, and CI. - Per-project isolation. Project A can run PHP 8.2 while Project B stays on PHP 7.4 with no conflict. Each project’s dependencies are completely sandboxed.
- Config-as-code portability. The environment is a text file committed to the repository. Onboarding a new developer means cloning the repo and running one command — no “install this extension, then change that ini setting” handoff document.
Option 1: Pure Docker with Docker Compose
Docker gives you the most control and the most transferable knowledge — the same docker-compose.yml pattern works for any stack, not just WordPress. The trade-off is a steeper initial setup. The version: key in Compose files is deprecated as of Docker Compose v2; omit it in new projects.
# docker-compose.yml — no "version:" key needed in Compose v2+
services:
db:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_db
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_password
volumes:
- db_data:/var/lib/mysql
wordpress:
depends_on:
- db
image: wordpress:php8.2-apache
restart: unless-stopped
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress_db
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_password
WORDPRESS_DEBUG: 1
volumes:
- ./wp-content:/var/www/html/wp-content # mount only your code
mailpit:
image: axllent/mailpit
ports:
- "8025:8025" # web UI
- "1025:1025" # SMTP
volumes:
db_data:
The mailpit service captures all outbound email from WordPress inside a local web UI at http://localhost:8025 — no email actually leaves your machine. Wire it up in wp-config-local.php:
# wp-config-local.php — add after WordPress DB constants
define( 'SMTP_HOST', 'mailpit' ); // container name is the hostname
define( 'SMTP_PORT', 1025 );
define( 'SMTP_FROM', 'wp@localhost' );
Starting the Environment and Running WP-CLI
# Start all containers in the background
docker compose up -d
# Tail logs (useful on first run to watch MySQL init)
docker compose logs -f
# Stop without destroying data
docker compose stop
# Stop and remove containers + volumes (full reset)
docker compose down -v
WP-CLI is not included in the official WordPress Docker image by default. You can either add a wpcli service to your compose file, or run commands via docker compose exec:
# Install WordPress core after containers are running
docker compose exec wordpress wp core install --url="http://localhost:8080" --title="My Local Site" --admin_user=admin --admin_password=password [email protected] --allow-root
# Import a production DB dump
docker compose exec -T db mysql -u wp_user -pwp_password wordpress_db < prod-backup.sql
# Search-replace the prod URL with the local one
docker compose exec wordpress wp search-replace "https://example.com" "http://localhost:8080" --allow-root --precise
The search-replace step is the one most developers forget when importing a production database locally — without it, every URL in the database still points at the live site and WordPress redirects you out of your local install on every click.
Option 2: Lando
Lando is a Docker wrapper built specifically for development workflows. It handles the container networking, SSL certificates, and tool integrations that you would otherwise configure manually. A single .lando.yml replaces several hundred lines of Docker configuration for a typical WordPress project.
# .lando.yml
name: myproject
recipe: wordpress
config:
php: "8.2"
webroot: .
database: mysql:8.0
xdebug: true # enable Xdebug 3 in one line
config:
php: .lando/php.ini # optional custom php.ini
tooling:
phpunit:
service: appserver
cmd: ./vendor/bin/phpunit
; .lando/php.ini — custom PHP settings applied inside the appserver container
upload_max_filesize = 128M
post_max_size = 128M
memory_limit = 512M
max_execution_time = 300
# Initialise a new project (run once in an empty folder)
lando init --recipe wordpress
# Start all containers
lando start
# Run WP-CLI commands directly (no exec needed)
lando wp core install --url="https://myproject.lndo.site" --title="My Local Site" --admin_user=admin --admin_password=password [email protected]
# Pull the database and uploads from a Pantheon/Kinsta/Acquia environment
lando pull --database=dev --files=dev
# Share your local site publicly via a tunnel (useful for client demos)
lando share
lando pull is the standout feature for client work — it downloads the database and file uploads from a connected hosting provider (Pantheon, Kinsta, Acquia) and runs the URL search-replace automatically. What would otherwise be a manual multi-step process becomes one command. For managing PHP dependencies inside the container, see the Composer for WordPress guide — Lando exposes lando composer the same way it exposes lando wp.
Option 3: LocalWP
LocalWP requires no command-line knowledge and no configuration files. Download the app, click “Create a new site,” choose a PHP version, and the site is running inside a .local domain in under two minutes. It is the fastest path from zero to a working WordPress install.
Key features worth knowing:
- Live Links — expose your local site publicly via a temporary URL for client demos or webhook testing.
- Blueprints — save any site (with plugins, theme, and settings pre-configured) as a template. Spinning up a new client project takes seconds.
- Built-in WP-CLI and SSH. Every site has a “Shell” tab that drops you into the container’s terminal:
# LocalWP bundles its own WP-CLI — open the site's "Shell" tab and run:
wp plugin install woocommerce --activate
wp search-replace "https://example.com" "http://mysite.local" --precise
# Export the DB for use on staging
wp db export --add-drop-table ~/Desktop/local-backup.sql
LocalWP’s main limitation is its hosting integrations: the one-click pull/push feature works natively only with WP Engine and Flywheel sites. If your production host is elsewhere, you manage database imports manually.
Comparison: Which Tool Fits Your Workflow?
| LocalWP | Lando | Pure Docker | |
|---|---|---|---|
| Learning curve | Lowest (GUI) | Medium (YAML + CLI) | Highest (full CLI) |
| Setup time | ~2 min (click & go) | ~5 min | 15–30 min (first time) |
| Per-project PHP version | Yes (dropdown) | Yes (php: "8.x") |
Yes (image tag) |
| Team portability | Good (share folder) | Excellent (.lando.yml) |
Excellent (docker-compose.yml) |
| Xdebug | Toggle in UI | xdebug: true in YAML |
Manual env vars |
| Email capture | Built-in (MailHog) | Add Mailhog service | Add Mailpit service |
| DB pull from hosting | Flywheel/WP Engine only | Pantheon, Kinsta, Acquia | Manual import |
| Best for | Freelancers, designers | Agencies, complex projects | DevOps, custom stacks |
The short version: reach for LocalWP when speed matters more than configurability. Use Lando when you need the environment definition committed to the repo and shared across a team. Use pure Docker when you need to match a non-standard production stack exactly, or when the project already has Docker infrastructure the local environment should mirror.
Adding Xdebug to a Docker Environment
Step debugging is the fastest way to understand what WordPress is actually doing inside a hook or filter. Xdebug 3 requires only a few environment variables added to the wordpress service in your compose file:
# docker-compose.yml — add an xdebug service override
wordpress:
environment:
XDEBUG_MODE: develop,debug
XDEBUG_CONFIG: >-
client_host=host.docker.internal
client_port=9003
start_with_request=yes
extra_hosts:
- "host.docker.internal:host-gateway" # Linux only; Mac/Win handle this automatically
With this in place, set a breakpoint in VS Code (with the PHP Debug extension) or PhpStorm and refresh the browser — execution pauses at the breakpoint with full variable inspection. Lando’s xdebug: true config option wires the same thing up automatically without the manual env vars. For the testing layer that pairs with step debugging, see the WordPress PHPUnit testing guide.
Local to Staging to Production Workflow
A containerized local environment is the first step in a complete deployment pipeline. The standard flow: develop locally → push to a feature branch → CI deploys to staging automatically → merge to main triggers a production deploy. For the full CI/CD setup, see the WordPress CI/CD pipeline guide. For creating and managing a staging environment, see the WordPress staging site guide.
The key discipline is keeping the staging and production environments as close as possible to local — which is exactly what defining the environment in a committed config file enforces. If staging runs PHP 8.2, your .lando.yml should say php: "8.2". The containerized local setup makes that easy to verify and enforce across the team.
Quick-Reference Checklist
- Drop the
version:key from newdocker-compose.ymlfiles — it is deprecated in Compose v2. - Mount only
./wp-content, not the fullwp-content/WordPress root — keeps the image slim and avoids permission issues. - Add a Mailpit or MailHog service to every local stack — never send real email from a dev environment.
- Always run
wp search-replaceafter importing a production database locally. - Commit
.lando.ymlordocker-compose.ymlto the repository — the environment definition is part of the codebase. - Use
lando pullfor staging/prod DB imports on supported hosts; usedocker compose execfor manual imports elsewhere. - Enable Xdebug via
xdebug: truein Lando or viaXDEBUG_MODEenv vars in Docker — step debugging saves hours.


