For freelancers and small agencies, hosting several client sites on one VPS is an attractive idea. A single well-configured server can comfortably run ten or twenty modest WordPress sites for less than the cost of managed hosting for two or three. Many people start exactly this way, and it works well, until the day one site is hacked and the malware spreads to every other site on the server, or one client’s traffic spike makes every site slow. Running multiple WordPress sites on one VPS can be safe, but only with the right separation.
The difference between a shared server that’s a liability and one that’s a solid platform is isolation. When you host multiple WordPress sites on one VPS, each site should be separated from the others in files, processes, databases and resources, so problems stay where they start. This article explains how to set that up on a typical Linux server running Nginx and PHP-FPM.
The default setup and its risks
The quick way to host several sites is to put them all in /var/www, owned by the www-data user, served by one PHP-FPM pool running as www-data, connecting to the database with one user that can access every database.
In that setup:
- A PHP vulnerability in one site lets an attacker read and write every other site’s files, because they all belong to the same user.
- The attacker can read every wp-config.php and therefore every database password.
- A traffic spike or a slow plugin on one site consumes all PHP workers, so every site slows down or returns errors.
- Debugging is harder, because logs and processes are mixed together.
Each of these can be fixed with a few configuration choices.
Layer 1: A separate Linux user per site
Create a system user for each site, and make that user the owner of the site’s files:
The 750 permission on the site’s home folder means other site users can’t even list its contents. The web server still needs to read static files, so add the Nginx user to the site’s group:
Now Nginx can serve the files, PHP for that site runs as clientone, and PHP for other sites, running as different users, can’t read them.
Layer 2: A separate PHP-FPM pool per site
PHP-FPM can run multiple pools, each with its own user, socket and limits. Create a pool file for each site, for example /etc/php/8.3/fpm/pool.d/clientone.conf:
Key points:
- user and group make PHP run as the site’s own user.
- pm.max_children caps how many PHP workers this site can use, so it can’t starve the others.
- pm = ondemand starts workers only when needed, saving memory for quiet sites. Busier sites may benefit from dynamic instead.
- open_basedir restricts which directories PHP can access, adding another barrier.
- Separate temporary, session and log paths keep each site’s data apart.
In the site’s Nginx configuration, point PHP requests to its own socket:
Reload PHP-FPM and Nginx after adding a pool.
Layer 3: A separate database and user per site
Each site gets its own database and its own database user that can only access that database, from localhost only:
Even if an attacker reads one site’s wp-config.php, they can’t access other sites’ data.
Layer 4: Resource limits
PHP-FPM worker limits are the first line of resource control. Size them so the total across all pools fits in memory. A rough calculation: if each PHP worker uses about 80 MB and you have 4 GB of RAM available for PHP, the sum of max_children across all pools should stay around 50, with a margin for the database and other services.
Beyond that, systemd can apply CPU and memory limits to individual pools if you run them as separate services, and cgroups can limit per-user resources. For most small setups, worker limits are enough.
Also consider per-site limits for:
- Upload size and execution time, set in each pool
- Disk usage, monitored per site so one site’s backups or logs can’t fill the disk
- Rate limiting in Nginx for login and XML-RPC endpoints per site
Layer 5: Separate logs
Give each site its own Nginx access and error logs and its own PHP error log. When something goes wrong, you can see immediately which site is involved. Fail2ban and other tools can also work per site.
Include the per-site log paths in logrotate.
Layer 6: Separate backups and credentials
Back up each site independently, so you can restore one without touching others. Store each site’s SFTP or SSH access separately, so a developer working on one client’s site has no access to another’s. If clients have their own access, chroot them to their site folder.
Caching and object caches
If you use Redis as an object cache, each site needs its own key prefix or its own Redis database number, otherwise sites can read each other’s cached data, which causes bizarre bugs and potential data leakage. Set WP_REDIS_PREFIX or the equivalent for your plugin to something unique per site, or run separate Redis instances for sensitive sites.
Page caches, whether Nginx FastCGI cache or plugin-based, should also use separate cache paths or keys per site.
Control panels can do this for you
Setting up isolation by hand is educational, but it’s also repetitive. Server control panels and management tools, such as CloudPanel, RunCloud, GridPane, SpinupWP, Ploi and others, create a separate system user, PHP-FPM pool and database per site automatically. They also handle SSL certificates, backups and PHP versions per site. For agencies hosting many client sites, one of these tools is often worth its cost in saved time and fewer mistakes.
If you use a panel, check that it actually creates separate users per site. Some create separate pools but run everything as the same user, which defeats much of the purpose.
When to split sites across servers
One VPS isn’t the right home for every site. Consider moving a site to its own server when:
- It regularly uses a large share of server resources
- It’s business-critical and its owner can’t tolerate any impact from other sites
- It has very different requirements, such as a large WooCommerce store with heavy traffic
- It handles sensitive data with compliance requirements
- The server has grown to the point where a single failure affects too many clients
A practical pattern is to keep many small brochure sites together on one or two shared servers and give larger stores and high-traffic sites their own servers.
A checklist for each new site
- Create a Linux user and home folder with restrictive permissions.
- Create a PHP-FPM pool running as that user, with worker limits and open_basedir.
- Create a database and database user scoped to that site.
- Create Nginx configuration with separate logs and the site’s PHP socket.
- Set a unique object cache prefix and cache path.
- Set up per-site backups.
- Add the site to monitoring.
Follow it every time, and you can host multiple WordPress sites on one VPS with confidence that each one is a separate room, not a shared open-plan office.
Monitoring per site
Shared servers benefit from monitoring that shows which site is responsible when something goes wrong. Set up uptime monitoring for every site separately, not just the server. Watch PHP-FPM pool status per site, which shows active and idle workers and how often a pool hits its max_children limit. A pool that frequently reaches its limit is a sign that the site needs more workers, better caching or its own server.
Disk usage per site matters too. A single site that starts storing large backups or logs inside its own folder can fill the disk for everyone. A weekly report of folder sizes per site, even from a simple du command in a cron job, catches these problems early.
Updates and maintenance windows
With many sites on one server, updating PHP or the operating system affects all of them at once. Keep a list of which sites run which PHP version and which plugins are known to be sensitive to PHP changes. Test upgrades on a staging copy of the most complex site first, because if it works there, the simpler sites almost always follow. Communicate maintenance windows to clients in advance, especially those with stores.
Security incidents on a shared server
If one site on the server is compromised, isolation contains the damage, but it’s still worth checking the others. Look for new files in other sites’ folders, unexpected processes running as other users and unusual database activity. Rotate the compromised site’s credentials and review whether any shared component, such as a common plugin version, could affect the other sites too.
Frequently asked questions
Is open_basedir enough on its own?
No. It’s a useful extra barrier, but separate Linux users are the main protection. open_basedir has been bypassed in the past and also adds slight overhead.
How many WordPress sites can one VPS handle?
It depends on traffic, plugins and caching. A 4 GB VPS with good page caching can serve many low-traffic brochure sites. Busy stores need much more.
Does each site need its own PHP version?
Not necessarily, but separate pools make it easy to run different PHP versions per site when needed, which helps during upgrades.
What about multisite instead?
WordPress multisite shares one codebase and database across sites. It suits related sites managed by one organisation, but it doesn’t provide the isolation needed for unrelated client sites.




