If you're running more than one WordPress site on shared hosting, you already know the pain: slow load times during traffic spikes, mysterious 500 errors when your host's server gets overloaded, and zero visibility into what's actually happening under the hood. The moment you graduate from a single blog to managing multiple WordPress installations — whether for your own projects, freelance clients, or an agency — a VPS becomes not just a nice-to-have but a genuine operational necessity.
In this guide, we'll walk through every step of hosting multiple WordPress sites on a single Ubuntu VPS using Nginx, from initial server sizing through PHP-FPM pool isolation, WP-CLI automation, SSL certificates, and backup strategies. Every configuration file is real and copy-pasteable. By the end, you'll have a production-ready multi-site server that's faster, more secure, and more cost-effective than any shared hosting plan.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $8.30/mo
Want fully managed hosting? — we handle everything
Why a VPS Beats Shared Hosting for Multiple WordPress Sites
Shared hosting works by packing dozens or even hundreds of websites onto a single server. The host oversells resources on the assumption that not every site will peak at the same time. That assumption breaks down the moment you start running multiple active WordPress installations, each with its own plugins, cron jobs, and database queries.
A VPS gives you three critical advantages for multi-site WordPress hosting:
Performance isolation. On shared hosting, another customer's runaway PHP process or database query can directly impact your sites. On a VPS, your allocated CPU and RAM are yours. If one of your WordPress installations spikes in traffic, it draws from your resources — not from a shared pool where someone else's poorly coded plugin is already consuming 80% of available memory.
Full configuration control. You choose your PHP version, your Nginx settings, your MySQL tuning parameters, and your caching strategy. Shared hosts lock you into their configuration. On a VPS, you can run PHP 8.3 with OPcache fine-tuned per site, configure Nginx FastCGI caching per domain, and allocate dedicated PHP-FPM pools to isolate sites from each other.
Cost efficiency at scale. A single shared hosting plan might cost $10–15/month for one site. Running five sites means five plans — $50–75/month for shared resources. A VPS with 4 vCPUs, 8 GB RAM, and 100 GB NVMe storage can comfortably host 10–20 WordPress sites for a fraction of that cost, with significantly better performance.
Server Sizing: VPS vs. VDS for WordPress Hosting
The right server size depends on how many sites you're hosting, the traffic each receives, and whether any run WooCommerce or other resource-heavy plugins.
For 1–5 lightweight WordPress sites (blogs, portfolio sites, small business pages), a Cloud VPS with 2 vCPUs, 4 GB RAM, and 60 GB NVMe storage is a solid starting point. This handles the LEMP stack, PHP-FPM pools for each site, and leaves room for caching and backups.
For 5–15 sites or any WooCommerce stores, step up to 4 vCPUs and 8 GB RAM. WooCommerce sites are substantially more resource-intensive than standard WordPress — every product page generates multiple database queries, and checkout processes involve real-time inventory checks and payment API calls.
For agencies managing client sites, consider a Cloud VDS with dedicated resources. Hosting client sites on shared resources introduces a risk: another tenant's CPU spike could slow your clients' sites. For agencies managing 5+ client sites or any WooCommerce store, dedicated resources on the Cloud VDS eliminate noisy-neighbor risk entirely. When a client's Black Friday sale drives a traffic spike, you have guaranteed CPU cycles — not a best-effort allocation from a shared pool.
The beauty of cloud-based infrastructure is that you can start small and scale up. Begin with a VPS, monitor resource usage for a few weeks, and upgrade if you're consistently hitting 70%+ CPU or RAM utilization.
Prerequisites: LEMP Stack Installation
Before setting up WordPress sites, you need a working LEMP (Linux, Engine-X, MySQL, PHP) stack on your Ubuntu 24.04 VPS. If you haven't already set this up, follow our complete guide: How to Install a LEMP Stack on Ubuntu 24.04.
At minimum, you need:
- Ubuntu 24.04 LTS with a non-root sudo user
- Nginx installed and running
- MySQL 8.0 (or MariaDB 10.11) installed and secured
- PHP 8.3 with FPM and WordPress-required extensions
Confirm everything is running:
sudo systemctl status nginx mysql php8.3-fpm
Install the PHP extensions WordPress requires if you haven't already:
sudo apt install php8.3-mysql php8.3-curl php8.3-gd php8.3-intl \
php8.3-mbstring php8.3-xml php8.3-zip php8.3-imagick php8.3-bcmath
Nginx Server Blocks for Each Domain
Each WordPress site gets its own Nginx server block (virtual host) configuration file. This allows Nginx to route requests for different domains to different document roots.
Create the document root directories for two example sites:
sudo mkdir -p /var/www/site1.com/public_html
sudo mkdir -p /var/www/site2.com/public_html
Set ownership so the web server user can read files and your deployment user can write them:
sudo chown -R www-data:www-data /var/www/site1.com/public_html
sudo chown -R www-data:www-data /var/www/site2.com/public_html
Server Block for site1.com
Create the configuration file:
sudo nano /etc/nginx/sites-available/site1.com
Add the following configuration:
server {
listen 80;
listen [::]:80;
server_name site1.com www.site1.com;
root /var/www/site1.com/public_html;
index index.php index.html;
# Logging
access_log /var/log/nginx/site1.com.access.log;
error_log /var/log/nginx/site1.com.error.log;
# Max upload size (WordPress media uploads)
client_max_body_size 64M;
# Main location block
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP processing via site-specific FPM pool
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
location = /wp-config.php {
deny all;
}
location ~* /wp-includes/.*\.php$ {
deny all;
}
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}
# Cache static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Block xmlrpc.php (common attack vector)
location = /xmlrpc.php {
deny all;
return 444;
}
}
Server Block for site2.com
sudo nano /etc/nginx/sites-available/site2.com
server {
listen 80;
listen [::]:80;
server_name site2.com www.site2.com;
root /var/www/site2.com/public_html;
index index.php index.html;
access_log /var/log/nginx/site2.com.access.log;
error_log /var/log/nginx/site2.com.error.log;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm-site2.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
location = /wp-config.php {
deny all;
}
location ~* /wp-includes/.*\.php$ {
deny all;
}
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location = /xmlrpc.php {
deny all;
return 444;
}
}
Enable both sites and test the configuration:
sudo ln -s /etc/nginx/sites-available/site1.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site2.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Notice that each server block points to a different PHP-FPM socket (php8.3-fpm-site1.sock and php8.3-fpm-site2.sock). We'll configure those pools next — this is the key to isolating sites from each other.
Separate MySQL Databases and Users per Site
Never share a single database between WordPress installations. Each site gets its own database and its own MySQL user with access only to that database. This prevents a compromised site from accessing another site's data.
Log into MySQL as root:
sudo mysql -u root -p
Create the databases and users:
-- Database and user for site1.com
CREATE DATABASE site1_wp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site1_user'@'localhost' IDENTIFIED BY 'S!t31_Str0ng_P@ssw0rd_2024';
GRANT ALL PRIVILEGES ON site1_wp.* TO 'site1_user'@'localhost';
-- Database and user for site2.com
CREATE DATABASE site2_wp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site2_user'@'localhost' IDENTIFIED BY 'S!t32_Str0ng_P@ssw0rd_2024';
GRANT ALL PRIVILEGES ON site2_wp.* TO 'site2_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Use genuinely strong passwords for each user — at least 20 characters with a mix of uppercase, lowercase, numbers, and symbols. A password manager like Bitwarden or KeePassXC makes this manageable. The passwords shown above are examples; generate your own.
Using utf8mb4 character set is essential for WordPress. It supports the full range of Unicode characters including emoji, which users will inevitably paste into content. The utf8mb4_unicode_ci collation provides proper case-insensitive sorting for international characters.
PHP-FPM Pool per Site
This is the most important isolation step. By default, all PHP requests are processed by a single PHP-FPM pool running as the www-data user. If site1.com has a vulnerability and gets compromised, the attacker's PHP code runs as www-data — the same user that has write access to site2.com's files.
Separate PHP-FPM pools solve this by running each site's PHP processes under a different system user, using a separate Unix socket. Even if an attacker achieves code execution on site1.com, they can't read or modify site2.com's files.
Create System Users for Each Site
sudo adduser --system --no-create-home --group site1
sudo adduser --system --no-create-home --group site2
Set file ownership so each site's files belong to its respective user:
sudo chown -R site1:site1 /var/www/site1.com/public_html
sudo chown -R site2:site2 /var/www/site2.com/public_html
PHP-FPM Pool for site1.com
Create the pool configuration:
sudo nano /etc/php/8.3/fpm/pool.d/site1.conf
[site1]
user = site1
group = site1
listen = /run/php/php8.3-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Process manager configuration
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
; Logging
php_admin_value[error_log] = /var/log/php-fpm/site1-error.log
php_admin_flag[log_errors] = on
; Security — restrict file access to this site's directory
php_admin_value[open_basedir] = /var/www/site1.com/public_html:/tmp
php_admin_value[upload_tmp_dir] = /tmp
php_admin_value[session.save_path] = /tmp
; WordPress-friendly PHP settings
php_value[upload_max_filesize] = 64M
php_value[post_max_size] = 64M
php_value[memory_limit] = 256M
php_value[max_execution_time] = 300
PHP-FPM Pool for site2.com
sudo nano /etc/php/8.3/fpm/pool.d/site2.conf
[site2]
user = site2
group = site2
listen = /run/php/php8.3-fpm-site2.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
php_admin_value[error_log] = /var/log/php-fpm/site2-error.log
php_admin_flag[log_errors] = on
php_admin_value[open_basedir] = /var/www/site2.com/public_html:/tmp
php_admin_value[upload_tmp_dir] = /tmp
php_admin_value[session.save_path] = /tmp
php_value[upload_max_filesize] = 64M
php_value[post_max_size] = 64M
php_value[memory_limit] = 256M
php_value[max_execution_time] = 300
Create the log directory and restart PHP-FPM:
sudo mkdir -p /var/log/php-fpm
sudo systemctl restart php8.3-fpm
Verify both pools are running:
sudo php-fpm8.3 -t
ls -la /run/php/php8.3-fpm-site*.sock
You should see both socket files listed. The listen.owner = www-data setting allows Nginx (which runs as www-data) to communicate with the socket, while the actual PHP processes run as the site-specific user.
A few notes on the pool tuning parameters:
- pm.max_children = 10 — Limits total PHP processes per site. On a 4 GB RAM VPS with two sites, 10 per site is conservative. Each PHP-FPM process typically uses 30–50 MB of memory for WordPress, so 20 total processes would consume 600 MB–1 GB.
- pm.max_requests = 500 — Recycles workers after 500 requests to prevent memory leaks from poorly written plugins from accumulating.
- open_basedir — This is critical. It restricts PHP's file system access to only the site's own directory and
/tmp. A compromised plugin on site1 literally cannot read files from site2's directory.
WordPress Installation with WP-CLI
WP-CLI is the command-line interface for WordPress. It makes installing, configuring, and managing WordPress vastly faster than clicking through the web installer — especially when you're setting up multiple sites.
Install WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info
Install WordPress for site1.com
# Download WordPress core
cd /var/www/site1.com/public_html
sudo -u site1 wp core download
# Create wp-config.php with database credentials
sudo -u site1 wp config create \
--dbname=site1_wp \
--dbuser=site1_user \
--dbpass='S!t31_Str0ng_P@ssw0rd_2024' \
--dbhost=localhost \
--dbcharset=utf8mb4
# Run the WordPress installation
sudo -u site1 wp core install \
--url="https://site1.com" \
--title="Site One" \
--admin_user=admin_site1 \
--admin_password='An0th3r_Str0ng_P@ss!' \
--admin_email=admin@site1.com
# Set permalink structure
sudo -u site1 wp rewrite structure '/%postname%/' --hard
Install WordPress for site2.com
cd /var/www/site2.com/public_html
sudo -u site2 wp core download
sudo -u site2 wp config create \
--dbname=site2_wp \
--dbuser=site2_user \
--dbpass='S!t32_Str0ng_P@ssw0rd_2024' \
--dbhost=localhost \
--dbcharset=utf8mb4
sudo -u site2 wp core install \
--url="https://site2.com" \
--title="Site Two" \
--admin_user=admin_site2 \
--admin_password='Y3t_An0th3r_P@ss!' \
--admin_email=admin@site2.com
sudo -u site2 wp rewrite structure '/%postname%/' --hard
WP-CLI runs every command as the site-specific user (via sudo -u site1), so all files are created with the correct ownership. No need to fix permissions afterward.
Install Common Plugins via WP-CLI
You can also install and activate plugins from the command line:
# For site1.com
cd /var/www/site1.com/public_html
sudo -u site1 wp plugin install redis-cache --activate
sudo -u site1 wp plugin install wordfence --activate
sudo -u site1 wp plugin install wp-super-cache --activate
# For site2.com
cd /var/www/site2.com/public_html
sudo -u site2 wp plugin install redis-cache --activate
sudo -u site2 wp plugin install wordfence --activate
sudo -u site2 wp plugin install wp-super-cache --activate
This is where WP-CLI truly shines for multi-site management. You can script the entire setup process, making it repeatable for every new site you add to the server.
SSL Certificates with Certbot for Multiple Domains
Every WordPress site needs HTTPS — for security, SEO, and because modern browsers flag HTTP sites as "Not Secure." Certbot with the Nginx plugin makes this straightforward even with multiple domains.
Install Certbot if you haven't already:
sudo apt install certbot python3-certbot-nginx
Request certificates for all domains at once, or one at a time:
# Certificate for site1.com (covers www and non-www)
sudo certbot --nginx -d site1.com -d www.site1.com
# Certificate for site2.com
sudo certbot --nginx -d site2.com -d www.site2.com
Certbot automatically modifies your Nginx server blocks to add SSL configuration and sets up a redirect from HTTP to HTTPS. It also installs a systemd timer for automatic renewal.
Verify auto-renewal is working:
sudo certbot renew --dry-run
After Certbot runs, your Nginx server blocks will be updated with SSL directives. The resulting configuration for site1.com will look something like this:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name site1.com www.site1.com;
root /var/www/site1.com/public_html;
ssl_certificate /etc/letsencrypt/live/site1.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site1.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ... rest of configuration unchanged
}
server {
listen 80;
listen [::]:80;
server_name site1.com www.site1.com;
return 301 https://$host$request_uri;
}
WordPress Hardening
A default WordPress installation has several security weaknesses. With multiple sites on one server, hardening each site is even more important — a compromised site could potentially be used as a staging point for lateral movement (though our PHP-FPM pool isolation significantly mitigates this).
File Permissions
Set correct permissions for each site. Directories should be 755 (readable and executable by all, writable only by owner), files should be 644 (readable by all, writable only by owner), and wp-config.php should be more restrictive:
# For site1.com
sudo find /var/www/site1.com/public_html -type d -exec chmod 755 {} \;
sudo find /var/www/site1.com/public_html -type f -exec chmod 644 {} \;
sudo chmod 600 /var/www/site1.com/public_html/wp-config.php
# For site2.com
sudo find /var/www/site2.com/public_html -type d -exec chmod 755 {} \;
sudo find /var/www/site2.com/public_html -type f -exec chmod 644 {} \;
sudo chmod 600 /var/www/site2.com/public_html/wp-config.php
wp-config.php Security Enhancements
Add these lines to each site's wp-config.php, before the "That's all, stop editing" comment:
// Disable file editing from WordPress admin dashboard
define('DISALLOW_FILE_EDIT', true);
// Disable plugin/theme installation from admin (deploy via WP-CLI instead)
define('DISALLOW_FILE_MODS', true);
// Force SSL for admin area
define('FORCE_SSL_ADMIN', true);
// Limit post revisions to reduce database bloat
define('WP_POST_REVISIONS', 5);
// Disable WordPress debug display in production
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', false);
// Set custom content directory (optional, adds obscurity)
// define('WP_CONTENT_DIR', dirname(__FILE__) . '/content');
// define('WP_CONTENT_URL', 'https://site1.com/content');
Generate fresh security keys for each site using the WordPress API:
cd /var/www/site1.com/public_html
sudo -u site1 wp config shuffle-salts
cd /var/www/site2.com/public_html
sudo -u site2 wp config shuffle-salts
Limit Login Attempts
WordPress's default login page accepts unlimited authentication attempts, making brute-force attacks trivially easy. You can handle this at the Nginx level, which is more efficient than a PHP plugin:
Add this to the http block in /etc/nginx/nginx.conf:
# Rate limiting for WordPress login
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=1r/s;
Then add this location block inside each site's server block:
location = /wp-login.php {
limit_req zone=wp_login burst=3 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
This limits login page requests to 1 per second per IP address, with a burst allowance of 3 requests. Legitimate users won't notice; brute-force bots will be stopped cold.
Backup Strategy for Multi-Site WordPress
Running multiple sites means multiple failure points. A solid backup strategy covers both databases and files, keeps backups on a separate disk or remote location, and is automated so you never forget.
Per-Site Database Backups
Create a backup script at /usr/local/bin/backup-wordpress.sh:
#!/bin/bash
# WordPress Multi-Site Backup Script
BACKUP_DIR="/var/backups/wordpress"
DATE=$(date +%Y-%m-%d_%H-%M)
RETENTION_DAYS=14
mkdir -p "$BACKUP_DIR"
# Array of sites — add new sites here
declare -A SITES
SITES[site1]="site1_wp site1_user"
SITES[site2]="site2_wp site2_user"
for SITE in "${!SITES[@]}"; do
read -r DB_NAME DB_USER <<< "${SITES[$SITE]}"
SITE_BACKUP_DIR="$BACKUP_DIR/$SITE/$DATE"
mkdir -p "$SITE_BACKUP_DIR"
# Database backup
mysqldump --single-transaction --quick --lock-tables=false \
"$DB_NAME" | gzip > "$SITE_BACKUP_DIR/database.sql.gz"
# File backup (WordPress files)
tar czf "$SITE_BACKUP_DIR/files.tar.gz" \
-C /var/www/"$SITE".com public_html \
--exclude='public_html/wp-content/cache'
echo "[$(date)] Backup completed for $SITE"
done
# Remove backups older than retention period
find "$BACKUP_DIR" -type d -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>/dev/null
echo "[$(date)] Backup rotation completed"
Make it executable and schedule it via cron:
sudo chmod +x /usr/local/bin/backup-wordpress.sh
sudo crontab -e
Add this line to run backups daily at 3 AM:
0 3 * * * /usr/local/bin/backup-wordpress.sh >> /var/log/wordpress-backup.log 2>&1
Off-Server Backups with rsync
Local backups protect against accidental deletion or corruption. They don't protect against disk failure or server loss. Use rsync to copy backups to a remote location:
# Sync backups to a remote server
rsync -avz --delete /var/backups/wordpress/ \
backup-user@remote-server:/backups/wordpress/
# Or sync to an S3-compatible bucket using rclone
sudo apt install rclone
rclone sync /var/backups/wordpress remote:wordpress-backups
Testing Restores
A backup that hasn't been tested is not a backup — it's a hope. Periodically test your restores:
# Test database restore
gunzip < /var/backups/wordpress/site1/2024-01-15_03-00/database.sql.gz | \
mysql -u site1_user -p site1_wp_test
# Test file restore
tar xzf /var/backups/wordpress/site1/2024-01-15_03-00/files.tar.gz \
-C /tmp/restore-test/
Performance Optimization for Multiple Sites
Once your sites are running, a few server-level optimizations can dramatically improve performance across all of them.
Object Caching with Redis
Redis stores frequently accessed database query results in memory, drastically reducing the load on MySQL. Install Redis once and share it across all sites using separate database indices:
sudo apt install redis-server
sudo systemctl enable redis-server
Configure a reasonable memory limit in /etc/redis/redis.conf:
maxmemory 256mb
maxmemory-policy allkeys-lru
For each WordPress site, add the Redis Object Cache plugin and configure it in wp-config.php with a unique database index:
// In site1.com's wp-config.php
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'site1_');
// In site2.com's wp-config.php
define('WP_REDIS_DATABASE', 1);
define('WP_REDIS_PREFIX', 'site2_');
Redis indexes (0–15 by default) keep each site's cache isolated. A cache flush on site1 doesn't affect site2.
OPcache Tuning
OPcache stores compiled PHP bytecode in shared memory, eliminating the need to parse and compile PHP files on every request. For multi-site WordPress, increase the defaults in /etc/php/8.3/fpm/conf.d/10-opcache.ini:
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
With 10 WordPress sites, you easily have 15,000+ PHP files. The default max_accelerated_files=10000 would overflow, forcing recompilation and negating OPcache's benefit.
Adding More Sites
Once you have this infrastructure in place, adding a new WordPress site is a repeatable process:
- Create the document root directory
- Create a system user for the site
- Create a PHP-FPM pool configuration
- Create a MySQL database and user
- Create an Nginx server block
- Install WordPress with WP-CLI
- Add SSL with Certbot
- Add the site to your backup script
After you've done it twice, the whole process takes about 10 minutes per site. You can even script steps 1–6 into a single shell script that accepts a domain name as an argument.
Growing Your Agency? Scale Your Infrastructure
As your portfolio of WordPress sites grows, your hosting needs will evolve. Here's the natural progression:
Starting out (1–5 sites): A Cloud VPS with 2–4 vCPUs and 4–8 GB RAM handles this comfortably. You're paying a fraction of what those sites would cost on shared hosting, with dramatically better performance.
Growing (5–15 sites): Move to a Cloud VDS with dedicated CPU and RAM resources. No noisy neighbors, guaranteed performance for your clients, and enough headroom for WooCommerce stores and high-traffic sites. Dedicated resources also make your performance metrics predictable — critical when you have SLA commitments to clients.
At scale (15+ sites or mission-critical): Consider Managed Dedicated Cloud Servers. At this point, the time you spend on server maintenance — security updates, performance tuning, backup monitoring, incident response — has a real opportunity cost. Managed hosting lets you focus on building client sites while the hosting team handles infrastructure.
The architecture we've built in this guide — isolated PHP-FPM pools, per-site databases, separated file ownership — scales cleanly from 2 sites to 20. The only things that change are the server resources underneath.
Conclusion
Hosting multiple WordPress sites on a VPS is not just about saving money — though you will save money. It's about having a professional-grade hosting setup where you control performance, security, and configuration. The PHP-FPM pool isolation we configured ensures that sites can't interfere with each other. The per-site database users prevent cross-site data access. The Nginx server blocks give you granular control over caching, rate limiting, and security headers for each domain.
Start with the LEMP stack, add sites methodically using the process outlined here, and monitor your resource usage as you grow. When the server starts feeling tight, you'll have a clear upgrade path from shared VPS to dedicated VDS to fully managed hosting — without changing your site architecture at all.