How to give each PHP website its own user and PHP-FPM pool

If all your sites run as the same user, one hacked plugin can read every other site's passwords and files. Giving each site its own user keeps a break-in to that one site.

The idea: each site gets its own Linux user, its own folder that only that user can open, and its own PHP-FPM "pool": a group of PHP workers running as that user. Nginx talks to each pool through its own socket file.

The examples use a site called shop.example with the user shop. Use your own names. The PHP version in paths is 8.3 on Ubuntu 24.04 and 8.4 on Debian 13; check yours with php -v.

1. Create a user for the site

sudo useradd --system --user-group --create-home \
  --home-dir /var/www/shop.example --shell /usr/sbin/nologin shop

This makes a user and a group called shop, with the site folder as its home. It can't log in with SSH.

2. Set up the folder and permissions

sudo mkdir -p /var/www/shop.example/{public,sessions,tmp}
sudo chown -R shop:shop /var/www/shop.example
sudo chmod 750 /var/www/shop.example
sudo chmod 700 /var/www/shop.example/{sessions,tmp}
sudo usermod -aG shop www-data

Put the website's files in public. The 750 on the site folder is what keeps other sites out: only shop and members of its group can open it. The last line adds the web server user to that group, so Nginx can still send images, CSS and other static files.

3. Create the PHP-FPM pool

sudo nano /etc/php/8.3/fpm/pool.d/shop.conf
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Start workers only when needed, at most 5 at once
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 30s

; Keep this site's sessions and uploads in its own folder
php_admin_value[session.save_path] = /var/www/shop.example/sessions
php_admin_value[upload_tmp_dir] = /var/www/shop.example/tmp
php_admin_value[sys_temp_dir] = /var/www/shop.example/tmp

Raise pm.max_children for busy sites. Each worker uses memory (often 30–80 MB), so add them up across all sites before you go higher. Our OPcache and PHP-FPM speed guide shows how to measure a worker and pick the number, and our out-of-memory guide shows how to keep PHP-FPM running if a worker uses too much.

4. Point Nginx at the pool

In the site's Nginx file (for example /etc/nginx/sites-available/shop.example), the PHP part becomes:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/shop.sock;
}

Set root to /var/www/shop.example/public.

5. Check and apply

sudo php-fpm8.3 -t
sudo nginx -t
sudo systemctl reload php8.3-fpm nginx

Both tests should say test is successful.

6. Turn off the shared pool

PHP comes with a pool called www that runs as www-data. Because www-data is now in every site's group, that pool could read all your sites. Once every site has its own pool, switch it off:

sudo mv /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/www.conf.disabled
sudo systemctl restart php8.3-fpm

Before you do, check that no Nginx site still uses it: grep -R php8.3-fpm.sock /etc/nginx/sites-enabled/ should print nothing.

7. Test that it works

Make a test file:

echo '<?php echo posix_getpwuid(posix_geteuid())["name"];' | sudo tee /var/www/shop.example/public/whoami.php

Open https://shop.example/whoami.php. It should show shop. Then delete the file:

sudo rm /var/www/shop.example/public/whoami.php

Repeat for each site

Every site gets its own user, folder, pool file and socket. Don't put two sites in one pool. Our guide to hosting several websites on one server shows how the rest fits together: one Nginx file, database and certificate per site, and how to remove a site cleanly.

Good to know

  • Files you upload by hand (with SFTP or git pull) should belong to the site user. Fix ownership with sudo chown -R shop:shop /var/www/shop.example. To let a client or developer upload, see SFTP access to one site only.
  • The site can change its own files. WordPress needs that for updates and uploads. If a site is hacked, the attacker can change that site, but not the others.
  • Cron jobs for the site should run as its user too: sudo crontab -u shop -e.
  • Database: give each site its own database user and password as well, with rights to its own database only.

Official sources

Next steps: block SSH password guessing with Fail2ban, then work through the security checklist for a new PHP server.

ServerNest is being built to set up a separate user and PHP pool for every site automatically. Join early access to try it first.

← Back to the security blog