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. 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.
- 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, is populated with root ownership; a bind mount carries the host directory’s ownership as-is. The fix differs in each case.
- 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 "$@"
- 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. - 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. - 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.