Skip to content

Uploads strategies

shipped 1.0.0

Set the default in cwp.yml under pull.uploads; override per run with cwp media pull --uploads <mode>.

ModeBehaviourWhen
proxyDefault. No transfer. Anything below /wp-content/uploads/ that is missing locally is served from the remote.Day to day. Turns a twenty-minute first setup into two minutes.
syncA full copy of the remote uploads directory. Correct, slow, large.When you need every asset locally: offline work, media edits.
skipNothing. Images are broken locally.Database-only work where the media does not matter.

How proxy works

cwp init writes .ddev/nginx/cwp-uploads-fallback.conf and every cwp db pull keeps it pointing at the environment you pulled from. nginx serves the local file when it exists and proxies to the remote when it does not.

It is nginx configuration, so it needs ddev restart. cwp says so whenever it writes or removes the file. The file belongs in git, not in .gitignore: a colleague who clones the project and runs ddev start gets the same behaviour without installing cwp.

Where nginx is not available, cwp falls back to a generated mu-plugin: webserver_type: apache-fpm, or a project whose .ddev/ cwp did not scaffold. A PHP filter can only rewrite URLs WordPress itself emits, so custom fonts, generated CSS and background images stored as URL strings 404 under it (B-019). In that case cwp db pull and cwp doctor both say which mechanism is running and what it does not cover. There is no silent downgrade to partial coverage.

cwp doctor verifies the mechanism rather than counting files: it requests one uploads path your database knows and your filesystem does not, and expects a 200. Under proxy almost every file is missing locally by design, so “there are attachments without files” says nothing. A fallback that exists but does not work looks exactly like one that does.

What proxy means for a builder that checks the disk

The fallback serves files to a browser. Code that asks the filesystem still finds nothing: is_file() is false for every proxied attachment, whatever the browser gets.

Bricks 2.4 asks before it renders. Media_Browser_Health calls get_attached_file() and tests it. An element whose file it cannot find draws a placeholder, and on the front end that renders nothing at all. Measured on a proxied install: a listing page showed 72 cards and 6 images, the six whose files happened to be on disk. The site logo was gone. Nothing on the page said why.

So the pull writes down what the remote holds, into a generated mu-plugin beside the fallback. The builder’s own filter answers from that list:

✓ uploads: known   1,024 file(s) recorded, so the builder can tell a proxied
                   file from a missing one

It is a membership test, taken once: no request per attachment, and no cache to invalidate. The two answers stay apart:

CaseWhat is trueWhat the builder is told
the remote has it, the fallback would serve itthe proxy worksunknown
the file is nowhere: not local, not remotegenuinely missingmissing, unchanged

A file that is gone everywhere still draws its placeholder. That is a broken reference, and the check exists to surface it.

The list ages. An attachment uploaded on the remote after your last pull is not in it and renders as missing until you pull again. cwp doctor says when the pull took it and from where:

✓ Uploads the builder can see   1,024 file(s), recorded 2026-08-28 from dev —
                                an upload made there since is not in it

Without a list, the builder checks the disk as it always has. When cwp cannot read the far side it writes none, and says so. A list that guesses would hide exactly the failure the builder’s check is for.

What proxy means for a Bricks export

A tool that reads the disk finds nothing there either, and Bricks’ design-system export is one: it embeds custom font binaries by reading them off the local filesystem.

cwp pull handles the design system’s own binaries itself. It fetches the few files the design system points at, then exports again. A proxied workspace therefore does not need sync to produce a complete tree. Where it cannot (no environment stands out, or --no-assets), it names each font it could not carry and cwp push refuses to send it.

To fetch media by hand, without the database:

cwp media pull dev --uploads sync

Privacy

proxy keeps the local copy coupled to production, and coupled widely: everything below uploads/ is one request away, not only the images WordPress renders. A scrubbed database plus a proxied uploads directory is a local copy with the personal data removed from the database and the production files still live.

sync and skip remove a fallback left over from an earlier pull rather than leaving it active. Use one of them when you need full separation from production.