Skip to content
Muhammet Şafak
tr
Asked by: Barış Answered:

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

  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, 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.

  3. Why chmod 777 isn’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

  1. 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. Set the setgid bit on the directory (chmod g+s) so new files keep the right group.

  2. 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 "$@"
  3. 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.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind