Understanding File Permissions
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.
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 and82:82
for Alpine) - We provide a script that can be called at build time to change the UID and GID of
www-data
(calleddocker-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:
Dockerfile
############################################
# Base Image
############################################
FROM serversideup/php:8.4-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:
docker-compose.yml
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.