Why 2 Websites on One Server Is the Most Common Setup Nobody Talks About
You have one VPS and two websites to run. Maybe it is a production site and its staging clone. Maybe it is your main domain and a client project you are maintaining. Maybe it is your business site and a personal portfolio. Whatever the combination, hosting two websites on a single server is the most practical arrangement for solo developers, small agencies, and VPS owners who do not want to pay for two separate machines.
This guide is specifically about two websites on one server — not twenty, not a hundred. The scope is intentional. Two sites have their own specific considerations: how you name your document roots, how you split PHP-FPM pools cleanly, how you handle SSL for both domains, and how you keep them isolated enough that one site cannot read the other's files. We will go through the entire setup step by step.
The Three Most Common 2-Site Scenarios
Before touching a config file, name your two sites clearly. The most common 2-site arrangements are:
Scenario 1: Production + Staging
Your live site runs at yourdomain.com. The staging environment lives at staging.yourdomain.com or yourdomain-staging.com. Both need SSL, both run the same PHP version, but they point to different databases and have different caching settings. This is the most technically demanding 2-site setup because the two sites need to be kept in sync while remaining fully isolated at the filesystem level.
Scenario 2: Production + Development
Similar to staging, but dev is messier. You might run PHP 8.4 on the dev site while prod is locked to 8.2 for stability. Dev might skip SSL entirely or use a self-signed cert. You want the dev site to be inaccessible from the public internet. This scenario requires per-site PHP version management and optional firewall rules to restrict dev access.
Scenario 3: Client A + Client B
Two unrelated websites for two different people or organizations. This is where isolation matters most. The clients should never be able to see each other's files, logs, or databases. Each site needs its own system user, its own PHP-FPM pool, its own MySQL user, and its own SSL certificate. If you ever hand the server off to a client, they should only see their own site.
How Name-Based Virtual Hosting Works
The fundamental mechanism behind hosting two websites on one server is name-based virtual hosting. When a browser connects to your server, it sends the domain name in the HTTP Host header (or the SNI extension in TLS). The web server reads this and routes the request to the correct document root.
site-a.com
your server IP
Host header
/var/www/site-a.com
the response
Both site-a.com and site-b.com point to the same IP address. The web server differentiates them entirely through the domain name in the request. This works for HTTP and HTTPS — modern TLS uses SNI (Server Name Indication) to present the correct certificate for each domain without needing separate IP addresses.
Step 1: Directory Structure
Set up a clean directory structure that makes both sites easy to manage and back up separately. For two sites, a flat structure under /var/www/ works well:
$ sudo mkdir -p /var/www/site-a.com/public_html
$ sudo mkdir -p /var/log/nginx/site-a.com
# Site B (staging, dev, or client B)
$ sudo mkdir -p /var/www/site-b.com/public_html
$ sudo mkdir -p /var/log/nginx/site-b.com
# Dedicated system users for isolation
$ sudo useradd -m -s /usr/sbin/nologin siteauser
$ sudo useradd -m -s /usr/sbin/nologin sitebuser
# Assign ownership
$ sudo chown -R siteauser:siteauser /var/www/site-a.com
$ sudo chown -R sitebuser:sitebuser /var/www/site-b.com
Step 2: Nginx Server Blocks for Both Sites
Create a separate Nginx config for each site. This keeps them modular — you can disable, reload, or troubleshoot one site without touching the other.
Site A Configuration
server {
listen 80;
listen [::]:80;
server_name site-a.com www.site-a.com;
root /var/www/site-a.com/public_html;
index index.php index.html;
access_log /var/log/nginx/site-a.com/access.log;
error_log /var/log/nginx/site-a.com/error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm-sitea.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Site B Configuration
server {
listen 80;
listen [::]:80;
server_name site-b.com www.site-b.com;
root /var/www/site-b.com/public_html;
index index.php index.html;
access_log /var/log/nginx/site-b.com/access.log;
error_log /var/log/nginx/site-b.com/error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm-siteb.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
$ sudo ln -s /etc/nginx/sites-available/site-a.com /etc/nginx/sites-enabled/
$ sudo ln -s /etc/nginx/sites-available/site-b.com /etc/nginx/sites-enabled/
# Test and reload
$ sudo nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo nginx -s reload
Step 3: Apache Virtual Hosts (Alternative)
If you prefer Apache, the equivalent uses VirtualHost blocks:
<VirtualHost *:80>
ServerName site-a.com
ServerAlias www.site-a.com
DocumentRoot /var/www/site-a.com/public_html
ErrorLog /var/log/apache2/site-a.com-error.log
CustomLog /var/log/apache2/site-a.com-access.log combined
<Directory /var/www/site-a.com/public_html>
AllowOverride All
Require all granted
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php8.3-fpm-sitea.sock|fcgi://localhost"
</FilesMatch>
</VirtualHost>
$ sudo apache2ctl configtest
Syntax OK
$ sudo apache2ctl graceful
Step 4: DNS — Two Domains, One IP
Both domains point to the same server IP. The web server handles the differentiation. Configure A records for each domain at your registrar or DNS provider:
| Domain | Record Type | Value |
|---|---|---|
| site-a.com | A | 198.51.100.50 (your server IP) |
| www.site-a.com | CNAME | site-a.com |
| site-b.com | A | 198.51.100.50 (same server IP) |
| www.site-b.com | CNAME | site-b.com |
For the production + staging scenario, staging typically uses a subdomain of the production domain instead of a separate domain — so staging.site-a.com would also need an A record pointing to the same IP.
Step 5: SSL for Both Domains
Let's Encrypt provides free certificates for both domains. Issue them separately so one certificate's expiry does not affect the other:
$ sudo apt install certbot python3-certbot-nginx -y
# Issue certificate for Site A
$ sudo certbot --nginx -d site-a.com -d www.site-a.com
# Issue certificate for Site B
$ sudo certbot --nginx -d site-b.com -d www.site-b.com
# Verify auto-renewal works for both
$ sudo certbot renew --dry-run
Congratulations, all renewals succeeded.
Step 6: PHP-FPM Isolation — The Critical Part
Running both sites under the same PHP-FPM pool is a security mistake. If Site B is compromised, the attacker can read Site A's files. For two sites, creating isolated pools takes ten minutes and the protection is complete.
[site-a]
user = siteauser
group = siteauser
listen = /var/run/php/php8.3-fpm-sitea.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 15
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
; Restrict file access to this site only
php_admin_value[open_basedir] = /var/www/site-a.com:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
[site-b]
user = sitebuser
group = sitebuser
listen = /var/run/php/php8.3-fpm-siteb.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 = 3
pm.max_requests = 500
; Restrict file access to this site only
php_admin_value[open_basedir] = /var/www/site-b.com:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
open_basedir directive prevents PHP from opening files outside the specified paths. Without it, a compromised script in Site B can read wp-config.php from Site A, including the database password. Ten minutes to configure, permanent protection.
Step 7: Different PHP Versions for Each Site
This is where two-site setups get interesting. If Site A runs a legacy app on PHP 8.1 and Site B runs a modern app on PHP 8.4, you can have both versions running simultaneously — each site gets its own pool on its own version.
$ sudo apt install php8.1-fpm php8.4-fpm -y
# Site A pool uses PHP 8.1
# listen = /var/run/php/php8.1-fpm-sitea.sock
# Site B pool uses PHP 8.4
# listen = /var/run/php/php8.4-fpm-siteb.sock
# Update Nginx server block for each site to match its socket path
This flexibility — different PHP versions per site on the same server — is one of the strongest reasons to move off shared hosting, where every site on the server shares one PHP version managed by the host.
Step 8: File Permissions
| Path | Owner | Permission | Purpose |
|---|---|---|---|
| /var/www/site-a.com/ | siteauser:siteauser | 750 | Site root, inaccessible to sitebuser |
| /var/www/site-a.com/public_html/ | siteauser:siteauser | 755 | Web-accessible directory |
| PHP files | siteauser:siteauser | 644 | Readable by web server, not executable |
| wp-config.php | siteauser:siteauser | 640 | Database credentials, restricted read |
| uploads/ | siteauser:siteauser | 755 | Writable by PHP-FPM for file uploads |
The key is that siteauser cannot read anything under /var/www/site-b.com/, and sitebuser cannot read anything under /var/www/site-a.com/. With correct ownership and 750 permissions on site roots, this is enforced at the kernel level without any additional tooling.
Step 9: Two Databases, Properly Separated
Each site should use its own MySQL or PostgreSQL database, with its own database user that only has access to that database. Cross-database access is a common misconfiguration in two-site setups.
mysql> CREATE DATABASE sitea_db CHARACTER SET utf8mb4;
mysql> CREATE DATABASE siteb_db CHARACTER SET utf8mb4;
mysql> CREATE USER 'sitea_user'@'localhost' IDENTIFIED BY 'strongpassA';
mysql> GRANT ALL PRIVILEGES ON sitea_db.* TO 'sitea_user'@'localhost';
mysql> CREATE USER 'siteb_user'@'localhost' IDENTIFIED BY 'strongpassB';
mysql> GRANT ALL PRIVILEGES ON siteb_db.* TO 'siteb_user'@'localhost';
mysql> FLUSH PRIVILEGES;
With this setup, even if siteb_user's credentials are exposed in a config file, the attacker can only access siteb_db — not sitea_db.
Resource Allocation: Sizing for Two Sites
Two sites on one server is comfortable even on modest hardware. Here is a realistic estimate of what each component consumes:
| Component | Site A | Site B | Total |
|---|---|---|---|
| PHP-FPM workers (idle) | 2 workers x 25MB = 50MB | 2 workers x 25MB = 50MB | ~100MB |
| PHP-FPM workers (peak) | 15 workers x 40MB = 600MB | 10 workers x 40MB = 400MB | ~1GB |
| MySQL shared | - | - | 300-500MB |
| Nginx | - | - | 50-100MB |
| OS overhead | - | - | 200-400MB |
A 2 GB VPS is adequate for two low-to-medium traffic WordPress sites. A 4 GB VPS gives comfortable headroom with caching enabled. If one site is predominantly static, the resource picture improves significantly.
Using Cgroups to Keep Sites from Fighting Over Resources
In a two-client scenario, you want a guarantee that if Site B has a traffic spike, it cannot starve Site A of CPU or memory. Linux cgroups enforce this at the kernel level:
$ sudo mkdir -p /sys/fs/cgroup/user.slice/user-sitea.slice
$ echo "50000 100000" > /sys/fs/cgroup/user.slice/user-sitea.slice/cpu.max
$ echo "536870912" > /sys/fs/cgroup/user.slice/user-sitea.slice/memory.max
$ sudo mkdir -p /sys/fs/cgroup/user.slice/user-siteb.slice
$ echo "50000 100000" > /sys/fs/cgroup/user.slice/user-siteb.slice/cpu.max
$ echo "536870912" > /sys/fs/cgroup/user.slice/user-siteb.slice/memory.max
The values above set a 50% single-core CPU limit and 512 MB memory cap per site. Adjust based on your server specs and the sites' traffic patterns.
Security Checklist for Two Sites on One Server
File-Level Isolation
- Separate system users per site (siteauser, sitebuser)
- Dedicated PHP-FPM pool per site with matching user
- open_basedir set in each pool config
- Site root permissions at 750
- Sensitive configs (wp-config.php) at 640
Network and Application
- Separate SSL certificate per domain
- Separate database user per site with scoped grants
- ModSecurity WAF on both vhosts
- Rate limiting per domain in Nginx
- Fail2ban watching both sites' access logs
Monitoring: Keeping an Eye on Both Sites
$ tail -f /var/log/nginx/site-a.com/access.log /var/log/nginx/site-b.com/access.log
# Check PHP-FPM pool status
$ sudo php-fpm8.3 -t
# Monitor resource usage by user
$ ps aux | grep -E "siteauser|sitebuser"
$ free -h && df -h
How Panelica Handles This Setup
Every step in this guide — Nginx config, PHP-FPM pool, SSL certificate, DNS zone, database user creation, file permissions, and cgroup limits — is something Panelica provisions automatically when you add a domain. The 9-step domain provisioning pipeline handles all of it, including assigning a dedicated system user with a unique UID, creating an isolated PHP-FPM pool (any version from 8.1 through 8.4 that you select), issuing a Let's Encrypt certificate, and setting up cgroup v2 resource limits. For two sites, it replaces perhaps two hours of manual setup with a few clicks.
If you are managing a production + staging pair, the built-in Git Manager and one-click WordPress staging tools handle the site-cloning workflow as well. For the client A + client B scenario, the RBAC system lets you give each client SFTP and panel access scoped only to their own domain — they log in and only see their own files, databases, and email accounts.
If you want to try it: panelica.com/free-trial — 14-day full trial, no credit card needed.
Summary
Hosting two websites on one server is straightforward when you follow a consistent pattern: separate document roots, separate system users, separate PHP-FPM pools with open_basedir, separate SSL certificates, and separate database users. The setup takes under two hours manually and works reliably on any VPS with 2 GB or more of RAM. The three scenarios — production/staging, production/development, and two client sites — each have slightly different requirements around PHP version management and access control, but the underlying architecture is the same in all three cases.
Related Posts
- Host Multiple Websites on One VPS: The Complete Guide — scaling from 2 sites to 20, 50, or 100
- WordPress Staging Environments From Your Hosting Panel
- Free VPS Control Panels in 2026: An Honest Comparison