❯ Local development
A real copy of production, minus the parts you should not have.
The local database is disposable and the tool rests on that. What it must never be is a second, unsupervised copy of your customers' data — or an 8 GB download.
Full media, without downloading it
Uploads stay where they are. A missing file falls through to the remote at the web-server level, which is the only layer that sees every request — the attachment API is not the only thing that asks for a file.
The browser asks the local site
GET /wp-content/uploads/2026/portrait.jpg — a plain request to the DDEV container.
nginx looks on disk first
location ^~ /wp-content/uploads/ with try_files $uri @cwp-uploads-remote. If the file is there, it is served and nothing else happens.
Otherwise it falls through to the remote
The named location proxies to the environment's URL, carrying the remote Host header — and no cookies.
The response comes back without its session
Any Set-Cookie header is dropped. The bytes arrive; the session does not.
It fetches files. It does not share a session.
^~ is load-bearing. A regex location beats a plain prefix match, and DDEV's WordPress configuration matches static assets by extension — so without it the fallback would apply to everything except the image and font requests it exists for.
Not a mu-plugin, which was the original design and was wrong. A PHP filter can only rewrite URLs WordPress itself emits — the attachment API. A .woff2 requested from an @font-face rule, or a background image stored as a URL string inside an element's settings, never passes through it.
The config file is committed, not gitignored: a clone has to serve uploads the same way without cwp installed. It takes effect after ddev restart.
Production data, without personal data
Every pull can scrub, and a pull installs the mail guard whether it scrubs or not. The two are separate on purpose: one protects the people in the database, the other protects them from your local site.
wp_users, as pulled
| user_login | user_email | display_name |
|---|---|---|
a.hartmann | a.hartmann@example.com | Anke Hartmann |
editor_02 | redaktion@example.com | Redaktion |
shop_kunde_881 | m.***@example.com | M. B. |
after cwp scrub
| user_login | user_email | display_name |
|---|---|---|
a.hartmann | a.hartmann@example.invalid | User 1 |
editor_02 | editor-02@example.invalid | User 2 |
shop_kunde_881 | user-881@example.invalid | User 3 |
Illustrative rows. Nothing on this page comes from anyone's database — which is rather the point of the feature.
The rule that makes the mail guard unavoidableThe scrub pipeline, in the design document