Understanding file permissions
The images run as the unprivileged www-data user. When you mount your code from the host, files the container writes belong to www-data's user ID. Files it needs to change must be writable by that ID. This page shows how to make the container's user match yours in development.
Why permissions break with mounted code
www-data is UID and GID 33 on Debian and 82 on Alpine. Your host user is usually something else, such as 1000, and every developer on a team may have a different ID.
With your code mounted as a volume:
- Files the container creates, such as logs and cache files, belong to
33:33on your host, so you needsudoto edit or delete them. - Directories owned by your host user may not be writable by
33, so PHP fails withPermission deniedwhen it writes tostorage/orbootstrap/cache/.
Why the images don't change the user at startup
Some images accept PUID and PGID variables and change the user when the container starts. That needs the container to start as root. Files created before the switch, such as early log files, can end up owned by root and no longer writable by www-data. These images stay unprivileged instead. You change the user ID when you build the image.
Match your user ID in development
Two helper scripts do the work at build time. Both must run as root:
docker-php-serversideup-set-id www-data <uid>:<gid>changeswww-data's UID and GID.docker-php-serversideup-set-file-permissions --owner <uid>:<gid>gives the image's own files back to that user:/var/www,/composer, and the web server's configuration, log, and certificate directories. It detects the variation, or you can pass--service.
Use a multi-stage Dockerfile so only the development target changes the user. Production keeps the default:
FROM serversideup/php:8.5-fpm-nginx AS base
FROM base AS development
ARG USER_ID
ARG GROUP_ID
USER root
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID
USER www-data
FROM base AS production
COPY --chown=www-data:www-data . /var/www/html
Build the development target with your IDs in Compose:
services:
php:
build:
context: .
target: development
args:
USER_ID: ${UID}
GROUP_ID: ${GID}
ports:
- 80:8080
volumes:
- .:/var/www/html
Compose reads these from your environment. Bash doesn't export UID and doesn't set GID at all. Export both before you run Compose. To make it permanent, add the line to your shell profile:
export UID GID=$(id -g)
docker compose up --build
www-data in the container now has your UID and GID. You can edit and delete the files it creates without sudo. Build the production target for deployment; it uses the default 33:33 (or 82:82 on Alpine).
Spin sets the user and group IDs for you if you'd rather not manage this yourself.