Customization

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:33 on your host, so you need sudo to edit or delete them.
  • Directories owned by your host user may not be writable by 33, so PHP fails with Permission denied when it writes to storage/ or bootstrap/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> changes www-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:

Dockerfile
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:

compose.yml
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:

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