dev-0's files/styles/ directory was root-owned — fixed live, worth understanding why
Area: infra / dev-0 filesystem Raised during: Session 2026-09-03 (getting B5's collection-viewing work live on dev-0 after merging PR #183 — see docs/sprints/sprint-02-theme-images-ui-and-endpoint-access.md, Workstream B5) Priority: Low — fixed live, not currently blocking anything. Worth a root-cause pass so it doesn't silently recur (e.g. on the next dev-0 rebuild or in staging/production).
What happened
After migrating real featured images onto dev-0's Group entities and clearing caches,
every collection card's thumbnail 500'd. drush watchdog:show showed:
Error: Failed to create style directory: public://styles/medium/public
ls -la web/sites/default/files/ showed every real file -rw-r--r-- owned by
www-data:www-data (correct — that's the PHP-FPM/webserver process user), but
web/sites/default/files/styles/ itself was drwxrwxr-x root:root. Group-write was
set, but www-data isn't a member of the root group, so the webserver process
couldn't create the medium/ subdirectory Drupal's image style system needs on first
use of any image style.
This was the first real usage of an image style anywhere in D11's Group content —
nothing before this session had a real image field on a Group entity, so nothing had
exercised this path yet. Not necessarily specific to Group content; any first use of
any image style on this container would have hit the same wall, if node/file image
styles hadn't already been exercised elsewhere (they likely have been, for shanti_image
or other content, which is presumably why this wasn't caught earlier).
Fix applied live (with explicit group agreement — Than, Yuji, Xiaoming)
sudo docker exec -u root mandala-drupal-0 chown -R www-data:www-data \
/opt/drupal/app/drupal/web/sites/default/files/styles
Confirmed fixed: the previously-500ing derivative URL now returns 200 image/webp.
ls -la after the fix shows styles/ as www-data:www-data, matching the rest of
files/.
Follow-up: persistence mechanism confirmed (2026-09-04)
Checked directly on dev-0 via docker inspect mandala-drupal-0 --format '{{json .Mounts}}':
sites/default/files (the whole tree, not styles/ specifically) is a bind mount,
not a Docker volume or part of the image's writable layer:
/mnt/data/mandala-drupal-0/sites/default/files → /opt/drupal/app/drupal/web/sites/default/files
(alongside separate bind mounts for keys/ and the SimpleSAMLphp dirs — no named
volumes anywhere in this container's mount list).
This resolves the "is the fix ephemeral" question: it isn't. The chown run from
inside the container as root wrote straight through to the host path — confirmed via
ls -la /mnt/data/mandala-drupal-0/sites/default/files/styles/ on the host, now
www-data:www-data (uid/gid 33) — so a container restart or redeploy against the same
bind mount will not reintroduce the root ownership; the fix is persisted at the
host filesystem, not the container layer.
Still open
- Root cause not investigated — why was
styles/root-owned while its sibling directories weren't, given the fix persists at the host path? Possibilities: created once by a root-context provisioning/deploy step (Ansible, container build) writing to/mnt/data/mandala-drupal-0/before the app ever ran, or a leftover from some earlierdocker exec -u rooton this host. Worth checkingterraform-infrastructure's Ansible playbooks / the Dockerfile for anywherestyles/(orfiles/generally) gets explicitly created, to fix it at the source rather than have it silently already-fixed on this one host. - Worth checking whether staging/production have (or will have) the same root-owned
styles/directory on their own/mnt/data/<container>/sites/default/files/host paths before this becomes a real incident there instead of a caught-early dev-0 finding — each host's bind-mount source is independent, so dev-0 being fixed says nothing about staging/production.