My non-root container can't write to the mounted storage directory—how do I fix the permissions?
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`?
Answer
Short answer: it’s not the permission bits, it’s a UID mismatch.
Short answer
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. I covered the other face of building the image non-root and as narrow as possible in the multi-stage and distroless record.
Why
-
Root cause: UID, not the username. Linux permissions care about the numeric UID/GID. The
appuser 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. -
Named volume or bind mount? A named volume is Docker-managed and, on first creation, copies the ownership and permissions of the image’s mount point (usually root, but that’s not guaranteed); a bind mount carries the host directory’s ownership as-is. The fix differs in each case.
-
Why
chmod 777isn’t the fix. 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 already enough; the problem isn’t that the permission bits are too tight, it’s that ownership sits in the wrong place.
What to do
-
Align the UID at build time for bind mounts. In the Dockerfile use
ARG UID/ARG GIDto create the user with your host’s UID, and copy the code withCOPY --chown=app:app. Ownership is then correct from the start, without even needing the entrypoint trick. Set the setgid bit on the directory (chmod g+s) so new files keep the right group. -
For named volumes, do a targeted chown + drop in the entrypoint. Start the container as root, chown only the directories that must be writable, then drop to the user:
#!/bin/sh # entrypoint (starts as root, then drops to app) chown -R app:app storage bootstrap/cache exec su-exec app "$@" -
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.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.