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