A reproducible WordPress environment needs two things pinned: the setup (core version, plugins, themes, options, content) and the stack (PHP version and extensions, database engine, web server). Most tooling pins one of them well and the other by accident, which is why an environment that is "in version control" still behaves differently for the person who checks it out.
This post is about which format pins what, and how to close the gap without maintaining a Docker file nobody on the team wants to own.
The three formats people actually use
There are three files in common use, and they operate at different altitudes.
docker-compose.yml declares infrastructure: images, volumes, networks, ports. It pins the stack precisely and says nothing about WordPress, so plugin and content setup ends up as a README or a shell script that drifts.
.wp-env.json declares WordPress concepts on top of Docker: core, plugins, themes, phpVersion, config constants, and mappings for mounting local directories. It is the best of the three at pinning both halves, and the price is that every contributor needs Docker and Node installed.
A Blueprint is a JSON build plan of ordered steps: install this plugin, write this file, set this option, run this PHP, run this WP-CLI command. It pins the setup and deliberately says very little about the stack, because it was designed to run in a browser.
None of the three is wrong. They answer different questions, and the mistake is assuming the one you chose covers both halves.
What a Blueprint actually pins
Blueprints came out of WordPress Playground and are now also the format WordPress Studio uses, which is what makes them worth learning: one file, more than one runtime.
A Blueprint declares preferred WordPress and PHP versions, plugins and themes to install and activate, site options, and arbitrary PHP, SQL, or WP-CLI steps to run at build time.
What it does not declare is the machine. "PHP 8.2" in a Blueprint is a request to the runtime, and what you get depends entirely on which runtime reads the file.
That is the whole gap in one sentence. The same Blueprint gives you SQLite in WebAssembly under Playground, Docker on a laptop under Studio, and a real Linux container on a server elsewhere.
Where the same Blueprint stops behaving the same
Run one file through different runtimes and the differences show up in predictable places.
Database engine. Playground runs SQLite rather than MySQL. Any runSQL step using MySQL-specific syntax, any plugin doing raw $wpdb queries, and anything relying on FULLTEXT indexes can behave differently or fail. We wrote about where Playground stops working in more detail.
Outbound HTTP. In the browser, WordPress HTTP requests become JavaScript fetch() calls, so a remote host that does not send permissive CORS headers is unreachable. A build step that downloads a ZIP from a vendor URL can therefore succeed on a server and fail in a browser for reasons that have nothing to do with your Blueprint.
PHP extensions. A step that needs imagick, intl, or a specific curl build is asking for something the runtime either has or does not. The Blueprint has no way to insist.
Persistence. Playground's storage is browser storage. Whether the site is still there tomorrow is a property of the runtime, not of the file.
So "the Blueprint works" is a claim about a Blueprint plus a runtime. Say both, or you have not said anything.
Common Blueprint authoring mistakes
The WordPress Playground team published an agent skill for writing Blueprints on 2 April 2026, and its list of frequent errors is a good checklist for humans too.
The three it calls out: using the wrong property name for a plugin resource, omitting the required refType when pointing a resource at a git branch, and forgetting require '/wordpress/wp-load.php' at the top of a runPHP step.
The first one is worth understanding rather than memorising. Step property names have changed across Blueprint versions, so a recipe copied from an older post can be valid JSON, wrong schema, and silently do nothing useful.
The fix is the same in every case: validate the document before you run it, not after a build has half-finished.
Running the same Blueprint on a real server
SandyWP reads the same Blueprint JSON and executes it in a real WordPress container with MariaDB behind it, so the setup half comes from your file and the stack half comes from a normal Linux server rather than from a browser.
Because the runtimes genuinely differ, it does not pretend a Playground recipe transfers untouched. Before a run, SandyWP reports what it changed in four categories:
- Natively provided. Playground flags that a real sandbox always supplies anyway are removed.
- Automatically adapted. Supported legacy resources and compatible steps are rewritten.
- Ignored and reported. Unsupported metadata or steps are dropped, and you are told which.
- Blocking. Schema errors, unknown feature keys, unsafe paths, and unresolved local resources stop the run.
Informational and warning findings do not block. Errors do, and a Blueprint with errors can still be saved so you can fix it in place.
Import it from the original blueprint.json URL when the recipe is self-contained, because relative resources resolve against that document's URL. Use a ZIP bundle when the recipe ships with local assets, with exactly one blueprint.json at the root or inside one top-level folder. Bundles are capped at 50 MiB compressed, 250 MiB expanded, and 2,000 file entries.
From the command line:
sandywp blueprint validate blueprint.json
sandywp blueprint bake plugin-recipe --wait
A saved Blueprint can also be applied to an existing ready sandbox from that sandbox's actions menu, which is useful when you want to layer a test setup onto a site you already have.
Authoring and validating Blueprints is free. Executing one runs PHP, SQL, and WP-CLI on a real machine, so it needs a paid plan.
Blueprint or snapshot: pick by what changes
A Blueprint is a build plan, which means it is reviewable in a pull request and re-runs from scratch every time. That is what you want while the setup is still changing, and it is what makes a bug report executable instead of a list of instructions nobody follows twice.
A Template is a snapshot of the finished site: WordPress and PHP versions, plugins, themes, files, and database, captured as they were. That is what you want once the setup has stopped changing and you are launching it fifty times a week.
The two are not rivals. Run the Blueprint once, bake the result into a Template, and keep the JSON in the repository as the readable source of truth.
Rebuilding from a Blueprint is also slower than restoring a snapshot, by roughly the time it takes to download and install everything in it. If a build step reaches out to a third-party URL, you have made your environment depend on that host being up.
What a reproducible environment still will not reproduce
Being honest about this is the difference between a useful test bed and a false sense of safety.
A reproducible environment does not reproduce load. Query patterns that only appear under concurrency will not show up.
It does not reproduce your host. Object cache, edge cache, WAF rules, open_basedir, and a stale OPcache entry are all production behaviour that no JSON file describes.
It does not reproduce anything bound to your domain. Licensed pro plugins, real payment gateways, OAuth callbacks, and email deliverability all key off the live hostname.
For those, use staging or a canary release. A Blueprint is the right tool for "everyone gets the same starting point", not for "this is exactly production".
A practical setup
Put blueprint.json in the repository next to the code it sets up. Review changes to it the same way you review code, because a change to the environment is a change to the product.
Validate it in CI so a broken recipe fails at the pull request rather than on someone's first day.
Then give people a way to run it that does not require them to install anything. For plugin and theme developers that usually means a URL a reviewer can click, which is where a hosted sandbox earns its place over a local Docker stack.
Keep the Blueprint as the source of truth. Bake a Template when the wait starts to annoy you.
