Tutorial

How to Host 2 Websites on One Server: Step-by-Step Setup

April 05, 2026

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

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.

$5
Monthly cost per site vs separate VPS
80%
Infrastructure cost reduction vs 2 dedicated VPS

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.

Browser requests
site-a.com
DNS resolves to
your server IP
Web server reads
Host header
Routes to
/var/www/site-a.com
Serves
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:

# Site A (production or client A)
$ 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

# /etc/nginx/sites-available/site-a.com
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

# /etc/nginx/sites-available/site-b.com
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;
    }
}
# Enable both sites
$ 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:

# /etc/apache2/sites-available/site-a.com.conf
<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 a2ensite site-a.com.conf site-b.com.conf
$ 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:

DomainRecord TypeValue
site-a.comA198.51.100.50 (your server IP)
www.site-a.comCNAMEsite-a.com
site-b.comA198.51.100.50 (same server IP)
www.site-b.comCNAMEsite-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:

# Install Certbot
$ 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.
SNI makes this work without extra IPs: Modern TLS uses Server Name Indication (SNI), which allows the server to present different SSL certificates for different domains — all on the same IP address. Every browser released after 2010 supports SNI. You do not need a dedicated IP per domain.

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.

# /etc/php/8.3/fpm/pool.d/site-a.conf
[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
# /etc/php/8.3/fpm/pool.d/site-b.conf
[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
$ sudo service php8.3-fpm restart
open_basedir is not optional: The 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.

# Install both PHP versions
$ 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

PathOwnerPermissionPurpose
/var/www/site-a.com/siteauser:siteauser750Site root, inaccessible to sitebuser
/var/www/site-a.com/public_html/siteauser:siteauser755Web-accessible directory
PHP filessiteauser:siteauser644Readable by web server, not executable
wp-config.phpsiteauser:siteauser640Database credentials, restricted read
uploads/siteauser:siteauser755Writable 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.

-- Create databases and users in MySQL
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:

ComponentSite ASite BTotal
PHP-FPM workers (idle)2 workers x 25MB = 50MB2 workers x 25MB = 50MB~100MB
PHP-FPM workers (peak)15 workers x 40MB = 600MB10 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:

# Set CPU and memory limits per site user (cgroups v2)
$ 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

# Watch both sites traffic in real time
$ 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

Security-first hosting panel

Hosting management, the modern way.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
When did you last test a restore?