@42wp/dev-env
v1.0.4
Published
42WP — a CLI to spin up disposable WordPress dev environments with Docker (Traefik + MySQL + per-project containers).
Maintainers
Readme
42wp — 42WP dev environments
O Guia do Mochileiro do WordPress — a tiny CLI that spins up disposable WordPress dev environments with Docker, the 42WP way.
42wp runs a shared global layer (Traefik reverse proxy + MySQL + phpMyAdmin +
Mailpit) and, on top of it, per-project WordPress containers reachable at
http://<project>.localhost. Your current working directory is mounted as
wp-content, so you develop your theme/plugin locally and WordPress runs in a
container with Xdebug, Memcached and WP-CLI preinstalled.
Requirements
- Docker with the Compose v2 plugin (
docker compose) - Node.js >= 18.17
Install
npm install -g @42wp/dev-envThis installs the 42wp command globally.
Usage
42wp init [project] # scaffold a .42wp-dev-env with this project's options
42wp global start # start Traefik + MySQL + phpMyAdmin + Mailpit
42wp start <project> # build & start a project (run from your theme/plugin repo)
42wp update <project> # update an existing project to a newer WordPress image
42wp wp <project> <args> # run a WP-CLI command inside the project container
42wp stop <project> # stop a project's containers
42wp rm <project> # remove a site (container, image, database) — keeps your repo
42wp seed <project> [n] # generate demo content in a running project (default 200)
42wp global stop # tear the global layer downRun 42wp with no arguments for the full command list.
Project config (.42wp-dev-env)
Drop a .42wp-dev-env file in your project repo to save its 42wp start options,
so you (and your team) can just run 42wp start with no flags. Scaffold one with
42wp init:
42wp init mysite --subdomains --vip --locale pt_BR --theme my-theme --demo-contentThat writes:
project_slug=mysite
multisite=subdomain # false | subdir | subdomain
username=admin
password=password
locale=pt_BR
default_theme=my-theme
wpvip=true
initial_content=20042wp start, run in that directory, reads the file and applies it — and CLI
flags override any value for a one-off run. With project_slug set you can run
42wp start with no name. 42wp init won't overwrite an existing file unless you
pass --force.
| Key | Effect |
| --- | --- |
| project_slug | project name (so 42wp start needs no argument) |
| multisite | false, subdir or subdomain |
| username / password | admin credentials (plaintext — local dev only) |
| locale | WordPress site language, e.g. pt_BR (pt-br is normalized) |
| default_theme | theme to wp theme activate after install |
| wpvip | clone + mount the VIP mu-plugins |
| initial_content | true (200 posts) or a number |
WordPress version
By default projects are built on wordpress:php8.4-apache (newest WordPress,
PHP 8.4, Apache). Pick a different image tag per project with --wp <tag>:
42wp start my-theme --wp latest # newest WP, image's default PHP
42wp start legacy --wp 6.9-php8.5-apache # pin a specific WP + PHP
42wp update my-theme # rebuild on the default tag, run wp core update-db
42wp update my-theme --wp php8.5-apache # update to a specific imageupdate rewrites the project's Dockerfile, re-pulls the base image, recreates the
container and runs wp core update-db to migrate the schema. Set a different
default for every project with the FORTYTWO_WP_TAG env var.
Removing a site
42wp rm <project> tears a site down completely — its containers, the locally
built image, and its database — and deletes the generated project dir
(~/.42wp/projects/<name>/, including any cloned VIP mu-plugins). Your
repository is never touched: wp-content is a bind mount of your working
directory, which Docker leaves alone. It asks for confirmation; skip the prompt
with --yes (or --force):
42wp rm my-theme # prompts before removing
42wp rm my-theme --yes # no prompt (for scripts/CI)Demo content
Seed a project with a full demo dataset using --demo-content (defaults to 200
posts), or set a count with --demo-content=<n>:
42wp start my-site --demo-content # ~200 posts
42wp start my-site --demo-content=50 # 50 postsIt runs a PHP seeder (wp eval-file) — no plugin required (FakerPress has no CLI) —
that creates:
- ~10
42wp_authorterms (Portuguese names), ~10 tags, ~10 categories - a small pool of real images downloaded from picsum.photos, reused as featured + inline images
- posts with rich HTML content (h1/h2/h3, lists, blockquote, images, links), a plain-text excerpt, varied past dates, and each linked to 1 category, 2–5 tags and 1–3 authors
Works on any project (not just --vip). It always creates the requested number of
posts (it doesn't skip when posts already exist), and a failure never aborts
start. The author terms use the 42wp_author taxonomy, which the project
registers itself (e.g. the 42-framework mu-plugin) — the seeder does not register
any taxonomy. If 42wp_author isn't available, the author links are simply skipped.
When the project ships the 42-framework (detected by client-mu-plugins/42-framework
or a composer.json that requires 42wp/wp-mu-plugins), the seeder additionally
configures — once, idempotently — a fuller site:
- the
/%category%/%postname%/permalink structure - a
42wp_homepagepost set as the static front page (falls back to a regular page if the type isn't registered) - 4 category-driven menus — Header Primário / Secundário and Footer Primário / Social (with a nested footer and YouTube/X/Instagram links) — assigned to the theme's registered menu locations
Re-running (or 42wp seed) reuses the existing homepage and menus instead of
duplicating them; only posts keep appending. On a plain WordPress site these extra
steps are skipped and only the content above is seeded.
To add demo content to an already-running project without recreating it, use
seed:
42wp seed my-site # 200 posts
42wp seed my-site 500 # 500 posts
42wp seed my-site --demo-content=50Admin credentials
The silent install creates an admin / password user by default. Override per
project on start:
42wp start my-theme --user joao --pass s3cr3tSite language
New sites install in English (en_US) by default. Pick a WordPress locale with
--locale; the language pack is downloaded and activated automatically:
42wp start my-site --locale pt_BR # Brazilian Portuguese
42wp start my-site --locale es_ES # Spanish (Spain)Set a default for every project with the FORTYTWO_WP_LOCALE env var. This is the
site language — distinct from --lang, which controls the 42wp CLI's own
messages (en/pt). The pack is downloaded into wp-content/languages; if the
download fails (e.g. offline), the site stays in English and start continues.
Multisite
Start a site as a WordPress network with --multisite (subdirectory) or add
--subdomains for a subdomain network:
42wp start my-network --multisite # sites at my-network.localhost/site2
42wp start my-network --multisite --subdomains # sites at site2.my-network.localhostThis installs WordPress, converts it to a network, writes the MULTISITE constants
into wp-config.php, and mounts a .htaccess with the matching network rewrite
rules (WP-CLI refuses to generate those for a multisite, which otherwise causes a
redirect loop on sub-sites). For subdomain networks the Traefik router is
widened to a wildcard (*.my-network.localhost) so sub-sites route to the
container; *.localhost already resolves to 127.0.0.1, so no /etc/hosts edits
are needed. Network admin lives at …/wp-admin/network/.
WordPress VIP (mu-plugins)
WPVIP sites expect the VIP Go mu-plugins under wp-content/mu-plugins. Pass
--vip to start and the tool clones
vip-go-mu-plugins-built
(shallow) and mounts it into the container at wp-content/mu-plugins:
42wp start my-vip-site --vipThe clone lives in the project's data dir (~/.42wp/projects/<name>/mu-plugins),
so it never touches your repo, and re-running start fast-forwards it. Regular
(non-VIP) projects are unaffected.
--vip also mounts a small dev-helper mu-plugin (00-42wp-dev.php) that loads
the WordPress admin media includes in admin/REST/CLI contexts. This fixes
"Call to undefined function media_handle_sideload()"-style errors from plugins
like FakerPress, which the VIP environment triggers by loading file.php early.
It's mounted from the data dir, so your repo and the cloned VIP suite stay clean.
Example
cd ~/code/my-theme
42wp global start
42wp start my-theme
# → http://my-theme.localhost (admin / password at /wp-admin)
42wp wp my-theme plugin list
42wp stop my-themeShared services once the global layer is up:
| Service | URL | | ----------- | ---------------------------- | | Traefik | http://localhost:8080 | | phpMyAdmin | http://db.42wp.localhost | | Mailpit | http://mail.42wp.localhost |
How it works
State lives under ~/.42wp (override with FORTYTWO_HOME):
~/.42wp/
docker-compose.global.yml # the global layer (created on first run)
mysql-data/ # persisted MySQL data
projects/<project>/ # generated wp-config.php, Dockerfile, docker-compose.yml, meta.json42wp start generates an ephemeral wp-config.php (fetching real salts from
api.wordpress.org, with a local fallback when offline), a Dockerfile, and a
docker-compose.yml, then builds the container and runs a silent
wp core install. The DB name is the project name with hyphens turned into
underscores; a trailing .suffix (e.g. .localhost) is stripped.
Where a project is bound. 42wp start mounts the directory you run it from as
wp-content, so it first checks that directory looks like one — it must contain
plugins/ and themes/, otherwise start refuses (rather than building a broken
site). Pass --force to override (e.g. bootstrapping a brand-new, empty project).
The chosen directory is recorded in the project's meta.json, and later starts
reuse it — so re-running from a different folder can't silently re-point the
container. Run 42wp start <name> --rebind from a new directory to move the binding
there on purpose.
The generated wp-config.php always defines WP_ENVIRONMENT_TYPE as local, so
wp_get_environment_type() reports local (not the WordPress default of
production) — code and plugins that branch on the environment behave as they
should in dev. The constant is set before mu-plugins load, so it holds even under
--vip. The config is regenerated every start, so to apply this to an
existing project, just re-run 42wp start <project>.
Configuration
| Env var | Default | Description |
| --------------- | -------------- | --------------------------------------------- |
| FORTYTWO_HOME | ~/.42wp | Where the global compose, DB data and projects live |
| FORTYTWO_WP_TAG | php8.4-apache | Default WordPress image tag for new projects (overridden by --wp) |
| FORTYTWO_WP_LOCALE | unset (en_US) | Default WordPress site language for new projects (overridden by --locale) |
| FORTYTWO_LANG | auto (LANG) | CLI UI language: en or pt |
| NO_COLOR | unset | Disable colored output |
You can also pass --lang en / --lang pt before the command.
License
MIT
