Paying for five separate hosting plans to run five low-traffic sites is money left on the table. One properly configured VPS handles them all, and every site performs better than it would on shared hosting.
Hosting multiple websites on one VPS works by running a web server like Nginx or Apache that reads the domain name in each incoming HTTP request and routes it to the correct site directory. Each site gets its own document root, database, SSL certificate, and PHP process pool. One server, one IP address, as many domains as your RAM and CPU can support.
This guide covers the complete setup: directory structure, Nginx server blocks, Apache virtual hosts, PHP-FPM pool isolation, free SSL with Certbot, security hardening, Redis caching, automated backups, and a clear framework for knowing when a single VPS is no longer enough.
Can You Host Multiple Websites on One VPS?
A single VPS can host anywhere from 3 to 20+ websites. The web server software handles all traffic routing. Nginx uses server blocks and Apache uses virtual hosts. Both work by reading the Host header in each HTTP request and matching it to a configured domain name. The matched domain maps to a specific directory on disk, where the site’s files live. One IP address, one server, many domains.
This works at the KVM virtualization layer too. Our Linux VPS 托管 plans run KVM-based isolation, meaning your VPS has a fixed slice of CPU and RAM that no other customer shares. That isolation is what makes multi-site consolidation on a VPS reliable, unlike shared hosting, where hundreds of accounts compete for the same physical resources with no guaranteed allocation.
How Many Sites Can One VPS Handle?
The honest answer depends on traffic volume and whether your sites use caching. Here is a practical reference based on real-world WordPress deployments:
| VPS Specs | Low-Traffic Sites (<500 visitors/day) | Medium-Traffic Sites (500–5,000/day) |
|---|---|---|
| 2 vCPU / 2 GB RAM | 3–5 sites | 1–2 sites |
| 2 vCPU / 4 GB RAM | 5–10 sites | 3–5 sites |
| 4 vCPU / 8 GB RAM | 10–20 sites | 5–10 sites |
The bottleneck is almost always RAM. Use this formula to size PHP-FPM workers:
max_children = available_RAM_for_PHP ÷ average_PHP_process_size_MB
Average WordPress PHP-FPM process: 30–50 MB. On a 4 GB VPS, reserve 1 GB for the OS and MariaDB. That leaves 3 GB for PHP, which supports roughly 60–100 total workers across all sites. With Redis object caching enabled, these numbers are conservative because Redis absorbs most database reads before PHP ever sees them.
Shared Hosting vs. VPS for Multiple Sites
Shared hosting puts hundreds of tenants on one machine with no resource guarantees. When one site spikes in traffic, every other site on that machine slows down. A VPS gives you a dedicated slice of CPU and RAM. PHP-FPM pools keep each site’s processes isolated so a traffic spike on site1.com cannot starve site2.com of workers.
Before You Start: Prerequisites
Get these in place before touching any configuration files:
- A VPS running Ubuntu 22.04 or 24.04 with root or sudo access
- At least 2 GB RAM (4 GB recommended for 5+ sites)
- Domain names registered with DNS A records pointing to your VPS public IP
- SSH key authentication configured and password authentication disabled
- Basic Linux command line familiarity (file editing, service management)
- UFW firewall installed
DNS A records push out across global resolvers within 24–48 hours. Test resolution with dig site1.com before running web server configuration. If the command returns your VPS IP, the domain is ready.
Set Up Your Directory Structure First
Most guides jump straight to web server configuration. Setting up a clean directory structure first saves hours of debugging later. Permissions, backup scripts, and log rotation all depend on a consistent layout.
Use this structure for every site:
/var/www/
├── site1.com/
│ ├── public/ ← document root (web-accessible files)
│ └── logs/ ← per-site access and error logs
├── site2.com/
│ ├── public/
│ └── logs/
Create the directories:
sudo mkdir -p /var/www/site1.com/{public,logs}
sudo mkdir -p /var/www/site2.com/{public,logs}
sudo chown -R www-data:www-data /var/www/site1.com/public
sudo chown -R www-data:www-data /var/www/site2.com/public
sudo chmod -R 755 /var/www
Separate log directories per site mean you can run tail -f /var/www/site1.com/logs/error.log to debug one site without seeing noise from all the others.
Create a Separate Linux User Per Site
Running every site under www-data is the default and the wrong choice for multi-site security. If one site gets compromised, a process running as www-data can read every other site’s files and database credentials.
A separate system user per site stops lateral movement after a compromise:
sudo adduser –no-create-home –shell /usr/sbin/nologin site1user
sudo adduser –no-create-home –shell /usr/sbin/nologin site2user
sudo chown -R site1user:www-data /var/www/site1.com/public
sudo chown -R site2user:www-data /var/www/site2.com/public
Each site’s PHP-FPM pool will run under its matching system user. A malicious script on site1.com runs as site1user, which has no read access to /var/www/site2.com/.
Install the Web Stack (Nginx + PHP-FPM + MariaDB)
Update the system and install the full stack on Ubuntu 24.04:
sudo apt update && sudo apt upgrade -y
sudo apt install nginx mariadb-server php8.3-fpm \
php8.3-mysql php8.3-curl php8.3-gd \
php8.3-mbstring php8.3-xml php8.3-zip -y
sudo mysql_secure_installation
Answer yes to all mysql_secure_installation prompts: set a root password, remove anonymous users, disallow remote root login, remove the test database, and reload privileges.
Nginx uses an event-driven, asynchronous architecture that handles thousands of concurrent connections with minimal memory. Apache’s process-based model uses more RAM per connection but remains the better choice for applications that rely on .htaccess files or per-directory configuration without server-level config access. For most modern PHP and WordPress deployments, Nginx is the right default.
Configure Nginx Server Blocks for Multiple Domains
Nginx reads the Host header on every incoming request and matches it against server_name directives in your configuration files. The matching block determines which document root serves the response. Multiple blocks share ports 80 and 443, and Nginx handles all routing internally.
Store each site’s configuration in /etc/nginx/sites-available/, then create a symlink to /etc/nginx/sites-enabled/ to activate it. This separation lets you disable a site without deleting its config.
Create a Server Block for Each Domain
Full production-ready configuration for site1.com. Create the file:
sudo nano /etc/nginx/sites-available/site1.com
Paste this configuration:
server {
listen 80;
listen [::]:80;
server_name site1.com www.site1.com;
root /var/www/site1.com/public;
index index.php index.html;
access_log /var/www/site1.com/logs/access.log;
error_log /var/www/site1.com/logs/error.log;
client_max_body_size 64M;
# Security headers
add_header X-Frame-Options “SAMEORIGIN” always;
add_header X-Content-Type-Options “nosniff” always;
add_header X-XSS-Protection “1; mode=block” always;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
}
# Block access to sensitive files
location ~ /\.ht { deny all; }
location = /wp-config.php { deny all; }
# Cache static assets at the browser level
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control “public, immutable”;
}
}
Repeat the same process for site2.com, changing server_name, root, access_log, error_log和 fastcgi_pass socket path.
Enable both sites, remove the default, and test:
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 rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
A clean nginx -t output confirms no syntax errors before you reload.
Add a Catch-All Server Block (The Security Step Most Guides Skip)
Without a catch-all default block, Nginx serves the first alphabetically-loaded config file to any request that does not match a known server_name. Automated scanners and bots probe VPS IP addresses constantly. Serving your site content to an unknown host header leaks information and can expose application details.
Add this block:
sudo nano /etc/nginx/sites-available/000-catch-all
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
sudo ln -s /etc/nginx/sites-available/000-catch-all /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Return code 444 closes the TCP connection immediately with no HTTP response. Scanners get nothing. The 000- prefix in the filename ensures this block loads before your site configs alphabetically, making it the true default.
Apache Virtual Hosts (For Legacy or .htaccess-Dependent Apps)
If your application requires .htaccess support or you are running legacy PHP software that expects Apache-style per-directory configuration, use Apache virtual hosts instead of Nginx server blocks.
Create the configuration file:
sudo nano /etc/apache2/sites-available/site1.com.conf
<VirtualHost *:80>
ServerName site1.com
ServerAlias www.site1.com
DocumentRoot /var/www/site1.com/public
ErrorLog /var/www/site1.com/logs/error.log
CustomLog /var/www/site1.com/logs/access.log combined
<Directory /var/www/site1.com/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Enable the site and reload:
sudo a2ensite site1.com.conf
sudo systemctl reload apache2
Repeat for each additional domain.
Configure Separate PHP-FPM Pools Per Site
The default www.conf PHP-FPM pool runs all PHP requests under www-data with a shared process pool. A traffic spike on site1.com or a runaway WordPress cron job can exhaust all available PHP workers and crash every other site on the server. Separate pools prevent this entirely.
Create a dedicated pool config for site1:
sudo nano /etc/php/8.3/fpm/pool.d/site1.conf
[site1]
user = site1user
group = www-data
listen = /run/php/php8.3-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
Duplicate this file for each site, incrementing the pool name, user, and socket path. Disable the default www.conf pool to prevent it from consuming resources:
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
Sizing guide: On a 4 GB VPS, reserve 1 GB for the OS and MariaDB. 3 GB remains for PHP. At 40 MB average per WordPress PHP-FPM process, you have room for roughly 75 total workers. Across 10 sites, set pm.max_children = 7 或 8 per site. Adjust based on each site’s traffic profile: a high-traffic site gets more workers, a low-traffic staging site gets fewer.
Verify the pools are running after restart:
sudo systemctl status php8.3-fpm
ls /run/php/
You should see one .sock file per configured pool.
Install SSL Certificates for All Domains
Every site on your VPS needs HTTPS. Let’s Encrypt provides free, automatically renewing SSL certificates through Certbot. Certbot covers all domains in a single command.
Install Certbot:
sudo apt install certbot python3-certbot-nginx -y
Generate certificates for all domains at once:
sudo certbot –nginx \
-d site1.com -d www.site1.com \
-d site2.com -d www.site2.com
Certbot modifies your Nginx server blocks automatically: it adds port 443 listeners, SSL certificate file paths, and HTTP-to-HTTPS redirect rules. After it runs, Nginx serves every site over HTTPS without any manual configuration changes.
Verify that auto-renewal works before trusting it:
sudo certbot renew –dry-run
A successful dry run confirms Certbot can reach Let’s Encrypt’s servers and renew certificates without human input. Certbot installs a systemd timer by default on Ubuntu. Check its status:
sudo systemctl status certbot.timer
SSL with Multiple Domains on One IP: SNI Explained
Running multiple SSL certificates on one IP address works through Server Name Indication (SNI). During the TLS handshake, the client’s browser sends the requested hostname before the HTTP connection is established. Nginx reads that hostname and serves the matching certificate. Every browser released after 2010 supports SNI. You do not need a separate IP address per domain, and adding IP addresses solely for SSL is unnecessary cost.
Harden Security Across All Sites
Enable UFW Firewall
A firewall limits which ports accept connections. Only open what your server actually needs:
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo ufw status
Everything else is blocked by default. If you run other services (mail, custom ports), add specific rules rather than opening broad ranges.
Install Fail2Ban
Fail2Ban monitors log files for repeated failed login attempts and bans the offending IP address automatically. It protects SSH by default and can be extended to WordPress login pages and other web endpoints.
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
Create a local jail configuration:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Set Correct File Permissions
The right permissions per resource type:
| Resource | Permission |
|---|---|
| Directories | 755 |
| PHP and HTML files | 644 |
| wp-config.php | 600 |
| Upload directories | 755 |
With each site running under its own system user, site2user cannot read files owned by site1user regardless of permissions. That separation is the structural protection. Permissions add a second layer.
Disable Nginx Version Disclosure
By default, Nginx includes its version number in HTTP response headers and error pages. Attackers use this to target known vulnerabilities in specific versions. Turn it off in /etc/nginx/nginx.conf inside the http {} block:
server_tokens off;
sudo nginx -t && sudo systemctl reload nginx
Add Redis Object Caching for WordPress Sites
Redis stores frequently queried database results in memory. For WordPress sites sharing a MariaDB instance, Redis can reduce database query load by 60–80% on read-heavy pages. That reduction directly increases how many concurrent visitors each site handles before hitting resource limits.
Install Redis:
sudo apt install redis-server -y
sudo systemctl enable redis-server
sudo systemctl start redis-server
The critical multi-site configuration detail that most guides miss: assign each site its own Redis database index. Redis provides 16 database indices (0–15) by default. Without different indices, cache keys from site1.com and site2.com can collide and serve the wrong cached content.
In each site’s wp-config.php, add the Redis database assignment before the /* That’s all, stop editing! */ comment:
// site1.com wp-config.php
define( ‘WP_REDIS_DATABASE’, 0 );
// site2.com wp-config.php
define( ‘WP_REDIS_DATABASE’, 1 );
// site3.com wp-config.php
define( ‘WP_REDIS_DATABASE’, 2 );
Install the “Redis Object Cache” plugin by Till Kruss in each WordPress site. Activate it and confirm the connection status shows “Connected” in the plugin settings. If you run more than 16 WordPress sites on one server, configure a second Redis instance on a different port.
Set Up Automated Backups
A backup strategy that is never tested is not a backup strategy. Automated database dumps and file archives cover the data layer. Off-site sync covers the catastrophic failure scenario.
Create the backup script:
sudo nano /usr/local/bin/backup-sites.sh
#!/bin/bash
BACKUP_DIR=”/var/backups/multi-site”
DATE=$(date +%Y-%m-%d)
mkdir -p “$BACKUP_DIR/$DATE”
# Databases
for DB in site1_db site2_db; do
mysqldump -u root “$DB” | gzip > “$BACKUP_DIR/$DATE/${DB}.sql.gz”
done
# Site files
for SITE in site1.com site2.com; do
tar -czf “$BACKUP_DIR/$DATE/${SITE}_files.tar.gz” \
-C “/var/www/$SITE” public
done
# Remove backups older than 14 days
find “$BACKUP_DIR” -type d -mtime +14 -exec rm -rf {} + 2>/dev/null
echo “Backup complete: $DATE”
Make it executable and schedule it:
sudo chmod +x /usr/local/bin/backup-sites.sh
echo “0 3 * * * root /usr/local/bin/backup-sites.sh” | sudo tee /etc/cron.d/multi-site-backup
For off-site storage, sync the backup directory to any S3-compatible object store using rclone:
sudo apt install rclone -y
# Configure rclone with your storage provider credentials
rclone sync /var/backups/multi-site remote:your-bucket/vps-backups
Add the rclone sync command to the backup script after the local archive step. Run a manual restore to a test domain every 90 days. A backup you have never restored is an assumption, not a guarantee.
Monitor Server Health Across All Sites
Monitoring becomes more important as the number of sites on a single VPS grows. One under-resourced site can create subtle performance degradation across all others before any site crashes outright.
Basic Monitoring with htop and vnstat
htop gives a real-time view of CPU and RAM usage per process. Sort by MEM% to identify which PHP-FPM pool is consuming the most memory. Press F6 to select the sort column.
sudo apt install htop vnstat -y
vnstat tracks bandwidth usage per network interface over time. Run vnstat -l for a live view or vnstat -d for daily totals. Useful for identifying which site drives outbound traffic spikes before you get a bandwidth overage notice.
Netdata for Live Server Dashboards
Netdata installs in one command and provides a browser-based dashboard with CPU, RAM, disk I/O, and Nginx request rate metrics updated every second:
wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh
sh /tmp/netdata-kickstart.sh
Access the dashboard at http://your-vps-ip:19999. Set alerts in /etc/netdata/health_alarm_notify.conf for RAM usage above 85% and disk usage above 85%. Those thresholds give you time to act before performance degrades or a disk fills completely.
For larger deployments running 10+ sites, Prometheus with Grafana provides longer retention and more granular per-site metrics through Nginx’s stub status module and PHP-FPM’s status endpoint.
WordPress Multisite vs. Separate WordPress Installs
Both approaches work on a VPS. The right choice depends on the relationship between your sites.
| 因子 | WordPress Multisite | Separate Installs |
|---|---|---|
| Admin overhead | One dashboard | One dashboard per site |
| Plugin conflict risk | High (plugins shared across all sites) | None (fully isolated) |
| Performance isolation | None (shared database tables) | Full (separate DB, PHP-FPM pool) |
| Traffic spike impact | Affects all sites in the network | Contained to one site |
| Theme and user management | Centralized | Independent |
| Best for | Related brand network, educational networks | Independent client sites, agency work |
Use WordPress Multisite when all sites belong to the same organization, share users or administrators, and use the same theme with minor customization per sub-site. A university with department sub-sites is the classic use case.
Use separate WordPress installs when you host sites for different clients, run projects with different traffic profiles, or need to update, troubleshoot, or migrate one site without touching the others. Separate installs give you complete fault isolation: a broken plugin on site3.com does not affect site1.com or site2.com.
For most agencies and developers running a mix of client sites and personal projects, separate installs with per-site PHP-FPM pools are the more stable and easier-to-maintain setup. The overhead of managing multiple WordPress admin panels is offset by the cleaner isolation.
When to Stop Consolidating and Move to a Dedicated Server
A multi-site VPS works until one of these conditions becomes consistently true:
- Any single site exceeds 50,000 monthly unique visitors and shares the server with 5+ other sites
- RAM usage stays above 85% for more than 2 hours during peak traffic
- CPU load average exceeds your VPS vCPU count for sustained periods (check with uptime)
- Compliance requirements (PCI-DSS, HIPAA) mandate physical hardware isolation between workloads
- You are managing 15+ WordPress installs and plugin update cycles have become a meaningful security risk
- Any site needs resources (RAM, CPU, disk throughput) that would deprive others of acceptable performance
At that point, move your highest-traffic sites to their own server. Our 专用服务器 start at $99/month with Intel Xeon processors, NVMe SSD storage, and 10Gbps ports, built for high-traffic workloads that have outgrown shared VPS resources. If you need full hardware isolation without management overhead, our 裸机服务器 provide dedicated physical hardware across 213+ global data centers.
For teams that still want the cost efficiency of a VPS but need significantly more headroom, our VPS Pro and VPS Ultimate plans scale to 4 cores and 16 GB RAM, which comfortably supports 15–20 low-traffic sites or 8–10 medium-traffic sites with Redis caching in place.
Vertical scaling (adding RAM and CPU to your current VPS) is the fastest path forward when you’re approaching limits. Horizontal scaling (splitting sites across multiple VPS instances behind a load balancer) makes sense when individual sites outgrow what a single server can handle.
常见问题
Can you host multiple websites on one VPS? Yes. A single VPS can host 3 to 20+ websites depending on traffic levels and resource allocation. Nginx server blocks or Apache virtual hosts route each domain to its own document root directory. PHP-FPM pools isolate PHP processes per site so no single site can exhaust workers for the others.
How many websites can a 4 GB VPS handle? A 4 GB KVM VPS comfortably runs 5 to 10 low-to-medium traffic WordPress sites with Redis object caching enabled. Reserve 1 GB for the OS and MariaDB, then allocate the remaining 3 GB across PHP-FPM pools at roughly 40 MB per worker. The real limit is RAM, not CPU, for typical content sites.
Is Nginx or Apache better for hosting multiple sites on one VPS? Nginx is the better default for multi-site VPS setups. Its event-driven architecture handles thousands of concurrent connections with less memory per connection than Apache’s process-based model. Apache is the right choice for applications that depend on .htaccess files or per-directory configuration without access to server-level config.
Do I need a separate IP address for each website? No. Server Name Indication (SNI) lets a single IP address serve different SSL/TLS certificates for multiple domains. The client sends the requested hostname during the TLS handshake, and Nginx selects the correct certificate before serving any content. All modern browsers support SNI. A separate IP per domain is unnecessary for HTTPS.
How do I stop one website from crashing all others on my VPS? Configure a separate PHP-FPM pool for each site with its own pm.max_children limit. Each pool runs under a dedicated system user with its own Unix socket. A traffic spike or runaway cron job on one site exhausts only that site’s allocated workers, leaving the other pools completely unaffected.
Can I host multiple WordPress sites on one VPS? Yes. Each WordPress install gets its own MariaDB database, PHP-FPM pool, Nginx server block, and SSL certificate. Assign each site a different Redis database index to prevent cache collisions. With 4 GB RAM and properly tuned PHP-FPM pools, 5 to 8 WordPress sites run without performance issues between them.
How do I back up multiple websites on one VPS? Run a cron-scheduled bash script that dumps each database with mysqldump, archives each site’s document root with tar, and syncs the output to off-site object storage with rclone. Schedule daily runs at a low-traffic hour (3 AM works well). Test restores every 90 days on a test domain.
Is hosting multiple sites on one VPS secure? Yes, with proper isolation in place. Run each site under a separate Linux user with its own PHP-FPM pool. Set file permissions so no user can read another’s files or credentials. Enable UFW, install Fail2Ban, and disable Nginx version disclosure. The main risk in an improperly configured setup is lateral movement after a single site compromise. Per-user isolation removes that risk.
How does DNS work when hosting multiple domains on one VPS? Create an A record for each domain pointing to your VPS’s public IP. When a visitor loads a domain, their browser sends an HTTP Host header with that domain name. Nginx reads the header and matches it against server_name directives to route the request to the correct document root and configuration block.
At what point should I upgrade from a VPS to a dedicated server? Move up once any site consistently exceeds 50,000 monthly visitors, RAM usage stays above 85% during peak hours, or compliance requirements demand physical hardware isolation. Atal Networks’ dedicated server plans provide isolated Intel Xeon hardware with NVMe SSD and 10Gbps ports for workloads that have outgrown shared VPS resources.
Run Multiple Sites Without Overpaying for Hosting
Hosting multiple websites on one VPS is the right setup for developers, agencies, and businesses running a portfolio of low-to-medium traffic sites. The configuration described here, Nginx server blocks, per-site PHP-FPM pools, separate Linux users, Redis caching, SSL via Certbot, and automated backups, scales from a two-site personal setup to a 15-client agency stack without fundamental changes. You adjust resource allocation, not architecture.
The key is starting with infrastructure that gives you full root access, consistent KVM-isolated resources, and room to scale when individual sites grow beyond what a shared VPS can support.
Atal Networks’ Linux VPS 托管 plans start at $5.25/month with KVM virtualization, full root access, and SSD storage across 213+ global data centers. All plans run on Dell hardware with Intel Xeon processors and ship with a 99.99% uptime SLA backed by our 100% network guarantee. Over 35,000 businesses across 196 countries run their infrastructure with us.
Once your sites outgrow a shared VPS, our 专用服务器 和 裸机服务器 are ready. No migration complexity. Same infrastructure provider, same support team, same SLA terms.


