Introduction

Hosting multiple websites on one VPS reduces costs and management overhead. Nginx server blocks (similar to Apache virtual hosts) allow serving different domains from the same IP address. This tutorial covers configuration, security isolation, and resource management for multi-site setups.

When to Use Multiple Sites Per VPS

Ideal for low-traffic sites (family blogs, small business sites, development projects). Also for microservices on subdomains (api.example.com, app.example.com). For high-traffic or security-sensitive sites (ecommerce, banking), use separate VPS. Hostxpeed VPS can handle 10-50 low-traffic WordPress sites with proper tuning (4GB RAM).

Prerequisites

One VPS with Nginx installed (sudo apt install nginx). Domain names pointed to VPS IP via DNS A records. Let's Encrypt for SSL (certbot). Basic understanding of Linux file permissions. Each site will have its own root directory under /var/www/ (e.g., /var/www/site1, /var/www/site2).

Step 1: Create Directory Structure

sudo mkdir -p /var/www/site1.com/public_html, sudo mkdir -p /var/www/site2.com/public_html. Set permissions: sudo chown -R $USER:$USER /var/www/site1.com/public_html, similarly for site2. Place test index.html: echo "

Site 1

" | sudo tee /var/www/site1.com/public_html/index.html. This ensures ownership for file uploads via SFTP.

Step 2: Configure Nginx Server Blocks

Create config file: sudo nano /etc/nginx/sites-available/site1.com. Content: server { listen 80; server_name site1.com www.site1.com; root /var/www/site1.com/public_html; index index.html index.htm index.php; location / { try_files $uri $uri/ =404; } location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; } }. Enable site: sudo ln -s /etc/nginx/sites-available/site1.com /etc/nginx/sites-enabled/. Test: sudo nginx -t. Reload: sudo systemctl reload nginx.

Step 3: Add PHP-FPM Pools per Site (Isolation)

Each site should have its own PHP-FPM pool for security and resource limits. Copy default pool: sudo cp /etc/php/8.2/fpm/pool.d/www.conf /etc/php/8.2/fpm/pool.d/site1.conf. Edit pool: change [www] to [site1], user = site1 (create corresponding system user), group = site1, listen = /var/run/php/php8.2-fpm-site1.sock, pm.max_children = 5 (adjust). Restart PHP-FPM: sudo systemctl restart php8.2-fpm. Update Nginx fastcgi_pass to this socket. Repeat for each site.

Step 4: Secure Separate Users for Each Site

Create system user per site: sudo useradd -r -s /bin/false site1, sudo useradd -r -s /bin/false site2. Set ownership of site directories: sudo chown -R site1:site1 /var/www/site1.com. Ensure PHP-FPM pool runs as that user. This prevents one compromised site from accessing another's files. Also set 750 permissions for directories (group read not needed). Use ACLs if fine-grained control needed.

Step 5: Add SSL with Let's Encrypt

Install certbot: sudo apt install certbot python3-certbot-nginx. Obtain certificate: sudo certbot --nginx -d site1.com -d www.site1.com. Certbot automatically modifies Nginx config to listen on 443 and redirect HTTP. Repeat for each domain. Auto-renewal active. For wildcard certificates, use DNS challenge (certbot certonly --manual --preferred-challenges dns).

Step 6: Configure Separate PHP.ini per Pool

Each pool can have custom php.ini directives (memory_limit, upload_max_filesize, etc.) by adding in pool config: php_admin_value[memory_limit] = 128M, php_admin_value[upload_max_filesize] = 64M. This isolates resource limits per site. For a site needing large uploads, increase accordingly without affecting others. Also set php_admin_flag[display_errors] = off in production.

Step 7: Database Isolation (Multiple Databases)

If using MySQL, create separate database and user per site: CREATE DATABASE site1_db; CREATE USER 'site1_user'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON site1_db.* TO 'site1_user'@'localhost';. Never share same database between sites. Use different prefixes if needed. For MariaDB, enable sql_mode to prevent cross-database access. Also limit user's resources: MAX_QUERIES_PER_HOUR 100000.

Step 8: Managing Logs Separately

In Nginx server block, set custom access and error logs: access_log /var/log/nginx/site1_access.log; error_log /var/log/nginx/site1_error.log. Then use logrotate for each (or configure global rotation). This simplifies debugging per site. Also set PHP-FPM log per pool: catch_workers_output = yes, php_flag[log_errors] = on, php_admin_value[error_log] = /var/log/php/site1_errors.log.

Step 9: Resource Limits and Monitoring

Use cgroups to limit CPU and memory per site (advanced). Simpler: set per-pool pm.max_children limits, and use Nginx limit_req module to throttle requests per site. Monitor with netdata (shows per-site PHP-FPM status). If one site starts using excessive resources, you can restart its PHP-FPM pool only: sudo systemctl restart php8.2-fpm (but restarts all pools). For granular, set up per-pool socket and use supervisor to manage individually.

Step 10: Automating Site Addition with Scripts

Create bash script to add new site: takes domain, username, creates directories, copies Nginx config template, creates PHP-FPM pool, adds to Nginx, reloads. Example bash script using sed. Saves time for agencies. Also integrate with ansible. Hostxpeed control panel allows adding domains (for managed hosting add-on), but manual method gives full control.

Security Best Practices

Disable PHP execution in upload directories: location ~* /(uploads|files)/.*.php$ { return 403; }. Prevent cross-site file access: set open_basedir per PHP pool: php_admin_value[open_basedir] = /var/www/site1.com:/tmp. This restricts PHP file access to its own directory. Also chroot Nginx for each server block? Not easily. Use AppArmor profiles. Regularly update all sites' CMS and plugins individually.

Performance Optimization for Multi-Site VPS

Use OpCache with sufficient memory (256MB) and separate cache keys per user? Not needed. Use Redis for object caching with separate database numbers per site (select 0,1,2). For MySQL, tune buffer pool for total database size. Enable Nginx caching for static assets globally. If sites share common frameworks (e.g., WordPress core), symlink wp-includes? Not recommended due to updates. Better use separate installations.

Backup Strategy for Multi-Site

Backup each site directory and its database separately. Script iterates over sites array. Restore per site independent. Exclude other sites' backups to save space. Use separate cron jobs. For disaster recovery, entire VPS snapshot works but restore all sites together. Prefer per-site backups for flexibility.

Troubleshooting Common Issues

Nginx 403 forbidden: check index file presence, directory permissions (755), user ownership. PHP not executing: verify fastcgi_pass socket path, PHP-FPM pool listening, socket permissions (usually 666). SSL cert for wrong domain: certbot --nginx for each domain separately. High memory: reduce pm.max_children per pool. Conflicting server_name order: first matching server block wins; use default_server directive. Log errors: tail -f /var/log/nginx/error.log.

Conclusion

Running multiple websites on one VPS is efficient and cost-effective. Use Nginx server blocks, separate PHP-FPM pools, dedicated system users, and per-site databases for isolation. Monitor resource usage and set limits. Start with 2-3 sites, then scale up sites count or upgrade VPS plan as needed. For mixed trust levels (e.g., untrusted third-party code), use separate VPS because PHP is not fully secure across users without additional hardening (suPHP, but performance heavy).