VPS Hosting for WordPress Agencies: Managing 50+ Client Sites

VPS Hosting for WordPress Agencies: Managing 50+ Client Sites

Managing one WordPress site on a VPS is straightforward. Managing 50 client sites is an operational challenge โ€” updates, backups, security patches, staging environments, client access control, and billing all need systems that scale. This guide covers the tools, architecture, and workflows that allow a WordPress agency to run dozens of client sites efficiently on a VPS.

Architecture Options for Multi-Site Agencies

Architecture Best for Pros Cons
All sites on one VPS Up to ~30 small sites Simple, cost-effective Single point of failure
Sites grouped by client tier 30โ€“100 sites Risk isolation per group More VPS to manage
Dedicated VPS per large client Enterprise clients Maximum isolation Higher cost per client
WordPress Multisite Sites with shared plugins/themes Central admin for all sites One breach affects all

๐Ÿ’ก VPS.DO Recommendation: For 20โ€“40 average WordPress sites, the USA VPS 30IPs plan (4 vCPU / 8 GB RAM / 500 GB SSD) provides both compute headroom and multiple IPs for client isolation. For 50+ sites, consider grouping clients across 2โ€“3 VPS instances. View Plans โ†’


Part 1: Server Setup for Multiple WordPress Sites

Step 1: Install the LEMP Stack

sudo apt update && sudo apt upgrade -y
sudo apt install nginx mariadb-server php8.3-fpm \
  php8.3-mysql php8.3-xml php8.3-mbstring php8.3-curl \
  php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath \
  php8.3-redis redis-server certbot python3-certbot-nginx \
  wp-cli -y

sudo systemctl enable nginx mariadb php8.3-fpm redis-server

Step 2: PHP-FPM Pool per Client

Isolate each client’s PHP processes โ€” prevents one buggy plugin from affecting others:

# Create a PHP-FPM pool for each client
sudo cp /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/client1.conf
sudo nano /etc/php/8.3/fpm/pool.d/client1.conf
[client1]
user  = client1
group = client1
listen = /run/php/php8.3-fpm-client1.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 500

Step 3: Nginx Config Template for Client Sites

nano /etc/nginx/templates/wordpress.conf.template
server {
    listen 443 ssl http2;
    server_name CLIENT_DOMAIN www.CLIENT_DOMAIN;
    root /var/www/CLIENT_SLUG;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/CLIENT_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/CLIENT_DOMAIN/privkey.pem;

    server_tokens off;
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    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-CLIENT_SLUG.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(css|gif|ico|jpeg|jpg|js|png|woff2|svg)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
        log_not_found off;
    }

    location ~ /\.(ht|git|env) { deny all; }
    location = /xmlrpc.php       { deny all; }

    access_log /var/log/nginx/CLIENT_SLUG.access.log;
    error_log  /var/log/nginx/CLIENT_SLUG.error.log;
}

Part 2: New Client Onboarding Script

Automate the entire setup of a new client site in minutes:

nano ~/new-client.sh
#!/bin/bash
set -e

DOMAIN=$1
SLUG=${DOMAIN//\./_}
DB_NAME="wp_${SLUG:0:20}"
DB_USER="${SLUG:0:16}_user"
DB_PASS=$(openssl rand -base64 16)
WP_ADMIN_PASS=$(openssl rand -base64 12)

if [ -z "$DOMAIN" ]; then
    echo "Usage: $0 yourdomain.com"
    exit 1
fi

echo "=== Setting up WordPress for: $DOMAIN ==="

# 1. Create system user
sudo useradd -m -s /bin/bash $SLUG 2>/dev/null || true

# 2. Create web root
sudo mkdir -p /var/www/$DOMAIN
sudo chown -R $SLUG:$SLUG /var/www/$DOMAIN

# 3. Create database
sudo mysql -u root -e "
    CREATE DATABASE IF NOT EXISTS $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';
    GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost';
    FLUSH PRIVILEGES;"

# 4. Install WordPress with WP-CLI
sudo -u $SLUG wp core download --path=/var/www/$DOMAIN
sudo -u $SLUG wp config create \
    --path=/var/www/$DOMAIN \
    --dbname=$DB_NAME \
    --dbuser=$DB_USER \
    --dbpass=$DB_PASS \
    --dbhost=localhost
sudo -u $SLUG wp core install \
    --path=/var/www/$DOMAIN \
    --url=https://$DOMAIN \
    --title="$DOMAIN" \
    --admin_user=admin \
    --admin_password=$WP_ADMIN_PASS \
    --admin_email=admin@$DOMAIN

# 5. Create PHP-FPM pool
sudo cp /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/$SLUG.conf
sudo sed -i "s/\[www\]/[$SLUG]/" /etc/php/8.3/fpm/pool.d/$SLUG.conf
sudo sed -i "s/user = www-data/user = $SLUG/" /etc/php/8.3/fpm/pool.d/$SLUG.conf
sudo sed -i "s/group = www-data/group = $SLUG/" /etc/php/8.3/fpm/pool.d/$SLUG.conf
sudo sed -i "s|listen = /run/php/php8.3-fpm.sock|listen = /run/php/php8.3-fpm-$SLUG.sock|" /etc/php/8.3/fpm/pool.d/$SLUG.conf
sudo systemctl reload php8.3-fpm

# 6. Create Nginx config
sudo cp /etc/nginx/templates/wordpress.conf.template /etc/nginx/sites-available/$DOMAIN
sudo sed -i "s/CLIENT_DOMAIN/$DOMAIN/g" /etc/nginx/sites-available/$DOMAIN
sudo sed -i "s/CLIENT_SLUG/$SLUG/g" /etc/nginx/sites-available/$DOMAIN
sudo ln -sf /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# 7. Install SSL
sudo certbot --nginx -d $DOMAIN -d www.$DOMAIN --non-interactive --agree-tos -m admin@youragency.com

# 8. Install essential plugins
sudo -u $SLUG wp plugin install redis-cache wordfence updraftplus --activate --path=/var/www/$DOMAIN

# Save credentials
cat >> ~/client-credentials.txt << EOF
=== $DOMAIN ===
WordPress Admin: https://$DOMAIN/wp-admin
Username: admin
Password: $WP_ADMIN_PASS
DB Name: $DB_NAME
DB User: $DB_USER
DB Pass: $DB_PASS
Date: $(date)

EOF

echo ""
echo "โœ… WordPress site ready: https://$DOMAIN"
echo "   Admin: admin / $WP_ADMIN_PASS"
echo "   Credentials saved to ~/client-credentials.txt"
chmod +x ~/new-client.sh
sudo bash ~/new-client.sh client1.com

Part 3: Mass Update Management with WP-CLI

WP-CLI is the command-line interface for WordPress โ€” essential for managing dozens of sites efficiently:

nano ~/update-all-sites.sh
#!/bin/bash
LOG="/var/log/wp-updates-$(date +%Y%m%d).log"
SITES=$(ls /var/www/ | grep -v html)

echo "=== WordPress Mass Update: $(date) ===" | tee $LOG

for SITE in $SITES; do
    if [ -f "/var/www/$SITE/wp-config.php" ]; then
        echo "--- Updating: $SITE ---" | tee -a $LOG

        # Update WordPress core
        wp core update --path=/var/www/$SITE 2>&1 | tee -a $LOG

        # Update all plugins
        wp plugin update --all --path=/var/www/$SITE 2>&1 | tee -a $LOG

        # Update all themes
        wp theme update --all --path=/var/www/$SITE 2>&1 | tee -a $LOG

        # Run database upgrades
        wp core update-db --path=/var/www/$SITE 2>&1 | tee -a $LOG

        echo "โœ… $SITE updated" | tee -a $LOG
    fi
done

echo "=== Update complete ===" | tee -a $LOG
chmod +x ~/update-all-sites.sh

# Schedule weekly updates (Sunday 3 AM)
crontab -e
# 0 3 * * 0 /bin/bash /root/update-all-sites.sh

Part 4: Automated Backups for All Client Sites

nano ~/backup-all-sites.sh
#!/bin/bash
DATE=$(date +%Y-%m-%d)
BACKUP_BASE="/var/backups/clients"
SITES=$(ls /var/www/ | grep -v html)

for SITE in $SITES; do
    if [ -f "/var/www/$SITE/wp-config.php" ]; then
        BACKUP_DIR="$BACKUP_BASE/$SITE/$DATE"
        mkdir -p "$BACKUP_DIR"

        # Backup files
        tar -czf "$BACKUP_DIR/files.tar.gz" /var/www/$SITE \
            --exclude=/var/www/$SITE/wp-content/cache \
            2>/dev/null

        # Backup database using WP-CLI
        wp db export "$BACKUP_DIR/database.sql" \
            --path=/var/www/$SITE 2>/dev/null
        gzip "$BACKUP_DIR/database.sql"

        echo "โœ… Backed up: $SITE"
    fi
done

# Remove backups older than 30 days
find $BACKUP_BASE -type d -name "20*" -mtime +30 -exec rm -rf {} + 2>/dev/null

echo "=== Backup complete: $DATE ==="
chmod +x ~/backup-all-sites.sh
# 0 2 * * * /bin/bash /root/backup-all-sites.sh

Part 5: Staging Environments

nano ~/create-staging.sh
#!/bin/bash
LIVE_DOMAIN=$1
STAGING_DOMAIN="staging.$1"
LIVE_SLUG=${LIVE_DOMAIN//\./_}
STAGING_SLUG="staging_${LIVE_SLUG}"

# Clone the live site to staging
sudo cp -r /var/www/$LIVE_DOMAIN /var/www/$STAGING_DOMAIN

# Create staging database
STAGING_DB="staging_${LIVE_SLUG:0:16}"
STAGING_PASS=$(openssl rand -base64 12)
sudo mysql -u root -e "
    CREATE DATABASE $STAGING_DB;
    CREATE USER '$STAGING_DB'@'localhost' IDENTIFIED BY '$STAGING_PASS';
    GRANT ALL ON $STAGING_DB.* TO '$STAGING_DB'@'localhost';"

# Export live DB and import to staging
wp db export /tmp/live-export.sql --path=/var/www/$LIVE_DOMAIN
mysql -u $STAGING_DB -p$STAGING_PASS $STAGING_DB < /tmp/live-export.sql

# Update staging wp-config.php
wp config set DB_NAME $STAGING_DB --path=/var/www/$STAGING_DOMAIN
wp config set DB_USER $STAGING_DB --path=/var/www/$STAGING_DOMAIN
wp config set DB_PASSWORD $STAGING_PASS --path=/var/www/$STAGING_DOMAIN

# Update URLs in staging database
wp search-replace "https://$LIVE_DOMAIN" "https://$STAGING_DOMAIN" \
    --path=/var/www/$STAGING_DOMAIN --all-tables

# Create Nginx config for staging
sudo cp /etc/nginx/sites-available/$LIVE_DOMAIN \
        /etc/nginx/sites-available/$STAGING_DOMAIN
sudo sed -i "s/$LIVE_DOMAIN/$STAGING_DOMAIN/g" \
        /etc/nginx/sites-available/$STAGING_DOMAIN
sudo ln -sf /etc/nginx/sites-available/$STAGING_DOMAIN \
            /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d $STAGING_DOMAIN --non-interactive \
  --agree-tos -m admin@youragency.com

echo "โœ… Staging ready at: https://$STAGING_DOMAIN"

Part 6: Security Monitoring Across All Sites

nano ~/security-scan.sh
#!/bin/bash
LOG="/var/log/security-scan-$(date +%Y%m%d).log"

for SITE in $(ls /var/www/); do
    if [ -f "/var/www/$SITE/wp-config.php" ]; then
        echo "=== Scanning: $SITE ===" | tee -a $LOG

        # Check for outdated WordPress
        WP_VERSION=$(wp core version --path=/var/www/$SITE 2>/dev/null)
        LATEST=$(wp core check-update --path=/var/www/$SITE --format=json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['version'] if d else 'up-to-date')" 2>/dev/null)

        echo "WordPress: $WP_VERSION (latest: $LATEST)" | tee -a $LOG

        # Check for plugins with known vulnerabilities
        wp plugin list --path=/var/www/$SITE \
            --fields=name,version,status \
            --format=table 2>/dev/null | tee -a $LOG

        # Check file permissions
        WORLD_WRITABLE=$(find /var/www/$SITE -type f -perm -o+w 2>/dev/null | wc -l)
        echo "World-writable files: $WORLD_WRITABLE" | tee -a $LOG
    fi
done

echo "=== Scan complete ===" | tee -a $LOG

Capacity Planning: How Many Sites Per VPS?

VPS RAM Idle sites Active (concurrent traffic)
4 GB 15โ€“25 sites 5โ€“8 sites with traffic
8 GB 30โ€“50 sites 10โ€“20 sites with traffic
16 GB 60โ€“100 sites 25โ€“40 sites with traffic

Fast โ€ข Reliable โ€ข Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DOโ€™s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!