One of the most cost-effective ways to use a Hong Kong VPS is to host multiple websites on a single server using Nginx virtual hosts. Instead of paying for a separate VPS for each domain, a single 4 GB VPS comfortably hosts 5–20 websites — each with its own domain, SSL certificate, PHP version, and database — while sharing the CN2 GIA network connection that gives all of them fast access from mainland China.
Architecture Overview
Internet (CN2 GIA)
↓
[Nginx on port 80/443]
Reads: Host header from request
↓ ↓ ↓
[site1.com] [site2.com] [site3.com]
/var/www/1 /var/www/2 /var/www/3
PHP 8.3-FPM Node.js 20 Static HTML
Each “server block” in Nginx matches the incoming Host header and routes to the appropriate web root, application backend, or upstream proxy.
Step 1: Install Nginx and Create Directory Structure
apt update && apt install -y nginx certbot python3-certbot-nginx ufw
# Create organised directory structure
mkdir -p /var/www/{site1,site2,site3}
# Each site gets its own logs directory
mkdir -p /var/log/nginx/{site1,site2,site3}
# Create separate system users per site (security isolation)
useradd -r -s /bin/false site1user
useradd -r -s /bin/false site2user
useradd -r -s /bin/false site3user
# Set ownership
chown -R site1user:www-data /var/www/site1
chown -R site2user:www-data /var/www/site2
chown -R site3user:www-data /var/www/site3
chmod -R 750 /var/www/site1 /var/www/site2 /var/www/site3
ufw allow 80/tcp && ufw allow 443/tcp && ufw enableStep 2: Virtual Host for Static HTML Site
cat > /etc/nginx/sites-available/site1.com << 'EOF'
server {
listen 80;
server_name site1.com www.site1.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name site1.com www.site1.com;
ssl_certificate /etc/letsencrypt/live/site1.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site1.com/privkey.pem;
root /var/www/site1;
index index.html;
access_log /var/log/nginx/site1/access.log;
error_log /var/log/nginx/site1/error.log;
location / {
try_files $uri $uri/ =404;
}
# Cache static assets aggressively
location ~* \.(css|js|png|jpg|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
EOF
ln -s /etc/nginx/sites-available/site1.com /etc/nginx/sites-enabled/
certbot --nginx -d site1.com -d www.site1.comStep 3: Virtual Host for WordPress / PHP Site
cat > /etc/nginx/sites-available/site2.com << 'EOF'
server {
listen 80;
server_name site2.com www.site2.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name site2.com www.site2.com;
ssl_certificate /etc/letsencrypt/live/site2.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site2.com/privkey.pem;
root /var/www/site2;
index index.php;
access_log /var/log/nginx/site2/access.log;
error_log /var/log/nginx/site2/error.log;
# WordPress-specific configuration
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
# Use site-specific PHP-FPM pool (see Step 5)
fastcgi_pass unix:/run/php/php8.3-fpm-site2.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~ /\. { deny all; }
}
EOF
ln -s /etc/nginx/sites-available/site2.com /etc/nginx/sites-enabled/
certbot --nginx -d site2.com -d www.site2.comStep 4: Virtual Host for Node.js Application
cat > /etc/nginx/sites-available/site3.com << 'EOF'
upstream site3_node {
server 127.0.0.1:3001;
keepalive 32;
}
server {
listen 80;
server_name site3.com www.site3.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name site3.com www.site3.com;
ssl_certificate /etc/letsencrypt/live/site3.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site3.com/privkey.pem;
access_log /var/log/nginx/site3/access.log;
error_log /var/log/nginx/site3/error.log;
location / {
proxy_pass http://site3_node;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection '';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
EOF
ln -s /etc/nginx/sites-available/site3.com /etc/nginx/sites-enabled/
certbot --nginx -d site3.com -d www.site3.com
nginx -t && systemctl reload nginxStep 5: Separate PHP-FPM Pools Per Site
Running all PHP sites through a single PHP-FPM pool means one site’s traffic affects all others. Separate pools provide isolation:
<code">cat > /etc/php/8.3/fpm/pool.d/site2.conf << 'EOF' [site2] user = site2user group = www-data listen = /run/php/php8.3-fpm-site2.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 5 pm.max_requests = 500 ; Restrict to site2's directory only php_admin_value[open_basedir] = /var/www/site2:/tmp php_admin_value[error_log] = /var/log/nginx/site2/php-error.log EOF systemctl restart php8.3-fpm
The open_basedir restriction is the key security feature — PHP running for site2 cannot read files from site1 or site3’s directories, even if a vulnerability in site2’s application attempts to access them.
Step 6: Separate MySQL Databases Per Site
mysql -u root -p << 'SQL'
-- Separate database and user per site
CREATE DATABASE site1_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE site2_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE site3_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site1db'@'localhost' IDENTIFIED BY 'Site1StrongPass!';
CREATE USER 'site2db'@'localhost' IDENTIFIED BY 'Site2StrongPass!';
CREATE USER 'site3db'@'localhost' IDENTIFIED BY 'Site3StrongPass!';
-- Each user can only access their own database
GRANT ALL ON site1_db.* TO 'site1db'@'localhost';
GRANT ALL ON site2_db.* TO 'site2db'@'localhost';
GRANT ALL ON site3_db.* TO 'site3db'@'localhost';
FLUSH PRIVILEGES;
SQLStep 7: Per-Site Log Rotation
<code">cat > /etc/logrotate.d/nginx-multisites << 'EOF'
/var/log/nginx/site1/*.log
/var/log/nginx/site2/*.log
/var/log/nginx/site3/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
nginx -s reopen
endscript
}
EOFStep 8: Resource Limits Per Site
Prevent a single high-traffic site from consuming all VPS resources:
<code"># Rate limit per site in Nginx (separate zones per site)
# In /etc/nginx/nginx.conf http block:
http {
limit_req_zone $binary_remote_addr zone=site1_limit:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=site2_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=site3_limit:10m rate=50r/s;
}
# In each site's server block, add:
# limit_req zone=site2_limit burst=20 nodelay;<code"># PHP memory limit per pool # In each pool's .conf file: php_admin_value[memory_limit] = 256M php_admin_value[max_execution_time] = 60
Monitoring Resource Usage Per Site
<code"># Check which sites are consuming the most requests tail -n 1000 /var/log/nginx/site1/access.log | wc -l tail -n 1000 /var/log/nginx/site2/access.log | wc -l # PHP-FPM process count per pool ps aux | grep php | grep "site2" | wc -l # Disk usage per site du -sh /var/www/site1 /var/www/site2 /var/www/site3 # Database size per site mysql -u root -p -e "SELECT table_schema, ROUND(SUM(data_length + index_length)/1024/1024,2) AS 'MB' FROM information_schema.tables GROUP BY table_schema;"
How Many Sites Can One VPS Handle?
| VPS RAM | Sites (low traffic) | Sites (medium traffic) |
|---|---|---|
| 2 GB | 3–5 | 1–2 |
| 4 GB | 8–12 | 3–5 |
| 8 GB | 20–30 | 8–12 |
| 16 GB | 50+ | 20–30 |
“Low traffic” = under 500 daily visitors per site. “Medium traffic” = 500–5,000 daily visitors with database queries on most pages. Sites with Redis object caching scale significantly higher than these estimates.
Conclusion
Nginx virtual hosting on a Hong Kong VPS makes hosting multiple websites economical — one server, one CN2 GIA connection, multiple domains each with full SSL and PHP isolation. The key to doing it safely is separate PHP-FPM pools with open_basedir restrictions and separate MySQL users — preventing cross-site contamination even if one application has a security vulnerability.
Host all your sites: Browse Server.HK Hong Kong VPS plans — a 4 GB plan with NVMe SSD comfortably hosts 8–12 low-to-medium traffic WordPress sites, all with CN2 GIA routing.