---
head.title: 'Understanding file permissions - Docker PHP - Server Side Up'
description: 'Eliminate the headache of file permissions when working with PHP + Docker.'
layout: docs
---

# Understanding File Permissions
::lead-p
Working with file permissions is one of the biggest headaches when working with PHP + Docker. This generally is because the PHP server also requires a web server to serve static files. By default, this means multiple users are created in the container, and permissions can get out of hand quickly.
::

![Traditional PHP File Permissions Configuration](/images/docs/permissions-privileged.png)

## Even more frustrating: Development Environments
Even if someone configured a single user in the container to run both the PHP server and the web server, things get even more complicated in development environments. For example, if you have Alice running her Windows Machine with WSL2, she might have a user ID of `1001`. Then you have Bob running his Ubuntu workstation with a user ID of `1002`. Meanwhile, Charlie is running his Docker on his macOS machine (that runs a tiny VM) that has a totally different file permission experience compared to Windows and Linux because of the file system differences.

If a volume is mounted from the container to the host, the container will write files to the host as `33:33`, which will require sudo/root permissions to edit and delete files.

## Our industry attempted workarounds
We've seen experiences that allow users to provide an environment variable of `PUID` and `PGID`. Although this is a great user experience, it requires the container user to be privileged, which is a major "no-no" in the security world. It also had downstream file permission errors if the container failed on initialization where logs would be created by the root user and no longer writable by the `www-data` user.

## Our solution
We focus on providing the tools to give sysadmins the ability to:
- Keep their containers unprivileged by default
- Allow the dynamic reconfiguration of the container user and group ID (at build time only)

It's a bummer that we can only set the user and group ID at build time, but it's a small price to pay for the security benefits of running unprivileged containers.

#### How it works
- By default, all our images run `www-data` as the user (`33:33` for Debian and `82:82` for Alpine)
- We provide a script that can be called at build time to change the UID and GID of `www-data` (called `docker-php-serversideup-set-id`)
- If you need to update permissions of service files (example: NGINX, Apache, Unit, etc), you can run the `docker-php-serversideup-set-file-permissions` at build
- We will use a multi-stage build to ensure that the `docker-php-serversideup-set-id` script is not executed in the construction of the final image

## Example
Here's an example of ensuring our UID/GID of `www-data` will match the development UID/GID of the host machine, while preserving the default UID/GID of `33:33` for the final image:

::code-panel
---
label: Dockerfile
---
```dockerfile
############################################
# Base Image
############################################
FROM serversideup/php:8.3-fpm-nginx-bookworm as base

############################################
# Development Image
############################################
FROM base as development

# Switch to root so we can do root things
USER root

# Save the build arguments as a variable
ARG USER_ID
ARG GROUP_ID

# Use the build arguments to change the UID 
# and GID of www-data while also changing 
# the file permissions for NGINX
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
    \
    # Update the file permissions for our NGINX service to match the new UID/GID
    docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx

# Drop back to our unprivileged user
USER www-data

############################################
# Production Image
############################################

# Since we're calling "base", production isn't
# calling any of that permission stuff
FROM base as production

# Copy our app files as www-data (33:33)
COPY --chown=www-data:www-data . /var/www/html
```
::

To show a simple Docker Compose file example for development, we could use:

::code-panel
---
label: docker-compose.yml
---
```yaml
services:
  php:
    build:
      context: .
      target: development
      args:
        # UID and GID must be set as environment variables on the host machine
        USER_ID: $UID
        GROUP_ID: $GID
    ports:
      - 80:8080
    volumes:
      - .:/var/www/html
```
::

When we run `docker compose up`, our compose file directs us to build the `development` target. This target will run the `docker-php-serversideup-set-id` script to change the UID and GID of `www-data` to match the host machine (assuming `$UID` and `$GID` are set in a zsh/bash profile or something similar). This will allow us to run the container as an unprivileged user while still having the correct permissions to read and write files.

The best thing is the user can delete files off of their machine without being prompted for sudo permissions. This is because we're aligning the UID/GID of the container with the host machine.

When it comes to building our image for production, we just use the `production` target, which will copy the files as `www-data` with the default UID/GID of `33:33`.

## An optimized experience from development to production
If you like the concepts above and you're looking for an optimized experience for developers (especially when it comes simplifying the setting of UID/GID), we recommend checking out our other open source project Spin. Spin is a lightweight wrapper for Docker Compose that allows you to manage your environment from development to production.

[Learn more about Spin →](https://serversideup.net/open-source/spin/)