VPS Hosting for WordPress Agencies: How to Host 20+ Client Sites on One Server
WordPress agencies face a hosting dilemma that individual site owners never encounter: managing dozens or hundreds of client sites efficiently, maintaining strong security isolation between clients, and delivering consistent performance — all while keeping infrastructure costs at a level that supports healthy agency margins. A well-configured VPS (or a small cluster of VPS instances) can host 20, 50, or even 100+ WordPress sites with the right architecture. This guide explains how to structure that infrastructure for scale, security, and operational efficiency.
Why Agencies Move to VPS (and Away from Managed Hosting Reseller Plans)
Many agencies start on managed WordPress hosting or reseller cPanel plans. These work well for individual sites but create operational problems at scale:
- Shared performance: Reseller plans pool resources, meaning a traffic spike on one client’s site affects others
- Limited customization: PHP version, server software, and caching configuration are often fixed by the host
- Margin compression: As client count grows, managed hosting costs scale linearly while VPS costs scale much more slowly
- Insufficient isolation: Security incidents on shared cPanel servers can affect multiple clients simultaneously
- Slow deployments: Managed hosting environments are often not compatible with modern CI/CD and Git-based deployment workflows
A self-managed VPS infrastructure eliminates all of these constraints, at the cost of more initial setup work and ongoing server administration responsibility.
Architecture Options for Multi-Site WordPress Agencies
Option 1: Nginx + PHP-FPM with Per-Site User Isolation
The most common approach for agencies hosting 10–50 sites on a single VPS. Each client site runs under its own Linux system user and PHP-FPM pool, providing process isolation between clients. File permissions prevent one client’s WordPress installation from reading or writing another’s files.
Advantages: Fine-grained control, minimal overhead per site, excellent performance
Disadvantages: More complex initial setup, requires server administration expertise
Option 2: Docker with Per-Site Containers
Each client site runs in its own Docker container with its own PHP, WordPress, and database configuration. Container isolation provides stronger security boundaries than user-based isolation.
Advantages: Stronger isolation, easy per-site customization (different PHP versions per client), portable between hosts
Disadvantages: Higher memory overhead per site, more complex management
Option 3: Control Panel (HestiaCP, CyberPanel)
An open-source control panel handles user creation, virtual host configuration, SSL, and database management through a web interface — reducing the command-line expertise required.
Advantages: Lower technical barrier, familiar interface for teams used to cPanel, faster site provisioning
Disadvantages: Less configuration control, control panels add software overhead
Recommended Server Specifications for Agency Hosting
| Sites Hosted | Monthly Traffic (combined) | Recommended VPS |
|---|---|---|
| Up to 20 sites | Under 500K pageviews | 4 vCPU / 8 GB RAM / 160 GB NVMe |
| 20–50 sites | 500K–2M pageviews | 8 vCPU / 16 GB RAM / 320 GB NVMe |
| 50–100 sites | 2M–10M pageviews | Multiple VPS with load balancer or dedicated server |
Traffic distribution matters as much as site count. Twenty low-traffic brochure sites use far fewer resources than five high-traffic e-commerce stores. Measure actual resource usage after initial deployment and scale accordingly.
Setting Up Per-Site User and PHP-FPM Isolation
Create a System User for Each Client
# Create user for client site
sudo adduser client1 --disabled-password --gecos ""
sudo mkdir -p /var/www/client1.com
sudo chown client1:client1 /var/www/client1.com
sudo chmod 750 /var/www/client1.com
Create a Dedicated PHP-FPM Pool
sudo cp /etc/php/8.2/fpm/pool.d/www.conf /etc/php/8.2/fpm/pool.d/client1.conf
sudo nano /etc/php/8.2/fpm/pool.d/client1.conf
[client1]
user = client1
group = client1
listen = /var/run/php/client1.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
sudo systemctl reload php8.2-fpm
Nginx Virtual Host for the Site
sudo nano /etc/nginx/sites-available/client1.com
server {
listen 443 ssl http2;
server_name client1.com www.client1.com;
root /var/www/client1.com;
index index.php;
ssl_certificate /etc/letsencrypt/live/client1.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/client1.com/privkey.pem;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/client1.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Automating Site Provisioning
Manually repeating these steps for every new client is time-consuming. Create a shell script that automates the entire provisioning process:
#!/bin/bash
# Usage: ./provision-site.sh clientname domain.com
CLIENT=$1
DOMAIN=$2
DB_PASS=$(openssl rand -base64 16)
WP_ADMIN_PASS=$(openssl rand -base64 16)
# Create system user
adduser $CLIENT --disabled-password --gecos ""
mkdir -p /var/www/$DOMAIN
chown $CLIENT:$CLIENT /var/www/$DOMAIN
# Create database
mysql -u root -e "CREATE DATABASE ${CLIENT}_wp;"
mysql -u root -e "CREATE USER '${CLIENT}'@'localhost' IDENTIFIED BY '${DB_PASS}';"
mysql -u root -e "GRANT ALL PRIVILEGES ON ${CLIENT}_wp.* TO '${CLIENT}'@'localhost';"
mysql -u root -e "FLUSH PRIVILEGES;"
# Download and configure WordPress
sudo -u $CLIENT wp core download --path=/var/www/$DOMAIN
sudo -u $CLIENT wp config create \
--path=/var/www/$DOMAIN \
--dbname=${CLIENT}_wp \
--dbuser=$CLIENT \
--dbpass=$DB_PASS
# Create PHP-FPM pool (copy template and substitute)
sed "s/CLIENTNAME/$CLIENT/g" /etc/php/8.2/fpm/pool.d/template.conf > \
/etc/php/8.2/fpm/pool.d/${CLIENT}.conf
systemctl reload php8.2-fpm
# Create Nginx vhost (copy template and substitute)
sed -e "s/DOMAIN/$DOMAIN/g" -e "s/CLIENTNAME/$CLIENT/g" \
/etc/nginx/sites-available/template > \
/etc/nginx/sites-available/$DOMAIN
ln -s /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# Issue SSL certificate
certbot --nginx -d $DOMAIN -d www.$DOMAIN --non-interactive --agree-tos -m admin@youragency.com
echo "Site provisioned. DB Password: $DB_PASS"
Redis Object Caching Across Multiple Sites
A single Redis instance can serve as the object cache for all WordPress sites on the server. Use Redis database numbers to separate each site’s cache namespace:
sudo apt install redis-server -y
In each site’s wp-config.php:
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 1); // Use a unique number per site (1-15)
define('WP_CACHE_KEY_SALT', 'client1_');
Automated Backups for All Client Sites
Create a backup script that runs daily for all client sites:
#!/bin/bash
# /usr/local/bin/backup-all-sites.sh
BACKUP_DIR=/var/backups/wordpress
DATE=$(date +%Y%m%d)
for DOMAIN_DIR in /var/www/*/; do
DOMAIN=$(basename $DOMAIN_DIR)
mkdir -p $BACKUP_DIR/$DOMAIN
# Backup files
tar czf $BACKUP_DIR/$DOMAIN/files-$DATE.tar.gz -C $DOMAIN_DIR . 2>/dev/null
# Backup database (extract DB name from wp-config.php)
DB=$(grep DB_NAME $DOMAIN_DIR/wp-config.php 2>/dev/null | \
grep -oP "(?<=')[^']+(?=')" | head -1) if [ -n "$DB" ]; then mysqldump $DB | gzip > $BACKUP_DIR/$DOMAIN/db-$DATE.sql.gz
fi
done
# Delete backups older than 14 days
find $BACKUP_DIR -name "*.tar.gz" -mtime +14 -delete
find $BACKUP_DIR -name "*.sql.gz" -mtime +14 -delete
sudo chmod +x /usr/local/bin/backup-all-sites.sh
# Run daily at 2 AM
echo "0 2 * * * root /usr/local/bin/backup-all-sites.sh" | sudo tee /etc/cron.d/wp-backups
Staging Environments per Client
Agencies need staging environments for testing updates before pushing to production. Create a staging subdomain for each client:
# Provision staging site using the same script with staging subdomain
./provision-site.sh client1-staging staging.client1.com
# Clone production database to staging
mysqldump client1_wp | mysql client1staging_wp
# Search-replace URLs in staging database
wp search-replace 'https://client1.com' 'https://staging.client1.com' \
--path=/var/www/staging.client1.com --all-tables
Monitoring All Sites from One Dashboard
Deploy Uptime Kuma to monitor all client sites with a single tool:
docker run -d --restart=always \
-p 127.0.0.1:3001:3001 \
-v uptime-kuma:/app/data \
--name uptime-kuma \
louislam/uptime-kuma:1
Add each client site as a monitored endpoint. Configure alert notifications to your team’s Slack channel so you hear about downtime before clients do.
Getting Started
For an agency hosting 20+ client sites, a 4 vCPU / 8 GB RAM VPS with NVMe storage is the recommended starting point. USA VPS plans at VPS.DO and Hong Kong VPS plans both include KVM virtualization, NVMe storage, and full root access — all necessary for the per-site isolation architecture described in this guide.
Conclusion
A properly architected VPS gives WordPress agencies far more control, better performance, and lower per-site cost than managed hosting reseller plans. Per-user PHP-FPM pool isolation, automated provisioning scripts, centralized Redis caching, and automated backup routines transform a single VPS into a professional multi-tenant WordPress hosting platform that scales with your agency’s client roster.