# My non-root container can't write to the mounted storage directory—how do I fix the permissions?

> It's not the permission bits, it's a UID mismatch: the mount's owner and the container user's UID don't match. The fix is aligning ownership, not chmod 777.

- Asked: 2026-08-13
- Answered: 2026-08-18
- Asked by: Barış
- Tags: docker, security, laravel
- Source: https://www.muhammetsafak.com.tr/en/just-ask/my-non-root-container-cant-write-to-the-mounted-storage-directory/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** I run my Laravel app in Docker and, for security, I switched from running the image as root to a non-root user I created. But after that, writes under `storage/` started failing: it can't write logs, cache and session files aren't created, and I keep getting "Permission denied."

My setup: an image based on `php:8.3-fpm-alpine`, application code baked into the image, but I mount the `storage/` directory as a volume for persistence. When the container ran as root there were no issues. How do I fix the permissions correctly for the non-root user without doing `chmod 777`?


Short answer: it's not the permission bits, it's a UID mismatch. The numeric UID of the user inside the container doesn't match the owner of the mounted directory; the reason it worked as root is that root (UID 0) can write anywhere. The fix is aligning ownership, not `chmod 777`.

1. **Root cause: UID, not the username.** Linux permissions care about the numeric UID/GID. The `app` user in your image might be UID 1000, but if the mounted directory is owned by a different UID on the host, the kernel refuses the write. Matching names means nothing.
2. **Named volume or bind mount?** A named volume is Docker-managed and, on first creation, is populated with root ownership; a bind mount carries the host directory's ownership as-is. The fix differs in each case.
3. **Targeted chown + drop in the entrypoint.** Start the container as root, chown only the directories that must be writable, then drop to the user:
```bash
#!/bin/sh
# entrypoint (starts as root, then drops to app)
chown -R app:app storage bootstrap/cache
exec su-exec app "$@"
```
4. **Align the UID at build time for bind mounts.** In the Dockerfile use `ARG UID`/`ARG GID` to create the user with your host's UID, and copy the code with `COPY --chown=app:app`. Ownership is then correct from the start, without even needing the entrypoint trick.
5. **Avoid chmod 777.** It's a security regression and defeats the whole point of going non-root. A group-writable bit (`g+w`) plus correct group membership is enough; set the setgid bit on the directory (`chmod g+s`) so new files keep the right group.
6. **Put persistent state in the right place.** If you'll run multiple replicas, don't share `storage/` over a shared filesystem — move sessions/cache to Redis and files to object storage like S3. The volume permission headache disappears entirely.

**Bottom line:** personally I'd pin the user with a build-time `ARG UID`, use `COPY --chown`, and for named volumes chown only `storage` and `bootstrap/cache` in the entrypoint before dropping to app with `su-exec`. Long term I'd move persistent data to S3/Redis wherever possible and keep the container genuinely stateless.
