A named volume for anything the host never reads

A named volume is managed by Docker and has no ownership relationship with the host, which removes the permission problem entirely for directories nobody edits.

services:
  php:
    volumes:
      - .:/app                        # source: bind, editable
      - vendor:/app/vendor            # named: not editable
      - node_modules:/app/node_modules
      - storage:/app/storage/framework

volumes:
  vendor: {}
  node_modules: {}
  storage: {}

Masking vendor and node_modules with named volumes is also a large performance win on macOS, where a bind mount of a hundred thousand small files is the slowest thing in the stack. The cost is that an editor on the host can no longer navigate into a dependency, which matters more than expected when debugging — a second, read-only bind mount at a different path is the usual compromise.