What you need before configuring Nginx
This guide uses Ubuntu 24.04 LTS, Nginx, PHP-FPM, MariaDB, and two example domains: site1.com and site2.com. Replace these names with your actual domains. The design works for static sites, WordPress, Laravel, PHP applications, documentation sites, and small APIs.
Minimum production requirements
- CPU: 4 vCPUs for several light websites; use dedicated physical cores for sustained traffic or CPU-heavy applications.
- Memory: 8 GB RAM minimum. Allocate more if several sites use PHP, MariaDB, Redis, or search services.
- Storage: 100 GB NVMe SSD minimum, with backups stored separately.
- Operating system: Ubuntu 24.04 LTS with root or sudo access.
- Network: A static public IPv4 address and correctly configured DNS.
- Access: An SSH client and a local copy of each website.
A VPS is sufficient for a few low-traffic sites, but a dedicated or bare-metal server is preferable for sustained traffic, large databases, strict isolation, or predictable disk and CPU performance. One server also creates a single failure domain, so maintain off-server backups and have a recovery plan.
Production capacity planning
Use the table below as a starting point for production sizing; actual capacity depends on caching, application code, database queries, image sizes, and traffic patterns.
| Scale: active sites and aggregate peak load | vCPU | RAM | Disk | Monthly bandwidth |
|---|---|---|---|---|
| 5 sites, up to 20 dynamic requests/sec | 4 vCPU | 8 GB | 160 GB NVMe SSD | 5 TB |
| 20 sites, up to 100 dynamic requests/sec | 8 vCPU | 32 GB | 480 GB NVMe SSD | 15 TB |
| 50 sites, up to 300 dynamic requests/sec | 16 dedicated cores | 64 GB | 960 GB NVMe SSD with RAID1 | 30 TB |
For sustained traffic, databases, or customers who require isolation, choose bare metal with dedicated cores and enterprise NVMe storage rather than oversubscribing a small VPS.
Step 1: Update Ubuntu and create an administrative user
Connect over SSH, update packages, and create a non-root account. Replace deploy with your preferred username.
ssh root@SERVER_IP
apt update && apt -y full-upgrade
apt install -y sudo
adduser deploy
usermod -aG sudo deploy
Copy your SSH key to the new account before disabling root login:
ssh-copy-id deploy@SERVER_IP
ssh deploy@SERVER_IP
sudo hostnamectl set-hostname web01.example.net
After confirming key-based login works, edit SSH settings:
sudo nano /etc/ssh/sshd_configSet PermitRootLogin no and, if every administrator uses keys, set PasswordAuthentication no. Validate and reload:
sudo sshd -t && sudo systemctl reload ssh
Step 2: Install Nginx, PHP-FPM, MariaDB, and firewall tools
Install the web stack and common PHP extensions. Remove extensions that your applications do not need.
sudo apt install -y nginx mariadb-server php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl unzip curl ufw fail2ban
sudo systemctl enable --now nginx mariadb php8.3-fpm fail2ban
sudo mariadb-secure-installationCheck the installed PHP-FPM socket because the version may differ on another Ubuntu release:
ls /run/php/php*-fpm.sock
systemctl status nginx --no-pager
Allow only required public services. SSH should be restricted to a trusted IP range where possible.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verboseStep 3: Create separate directories and permissions
Give every website its own document root. Keeping sites separate simplifies deployment, permissions, logs, and future migration.
sudo mkdir -p /var/www/site1.com/public
sudo mkdir -p /var/www/site2.com/public
sudo chown -R deploy:www-data /var/www/site1.com /var/www/site2.com
sudo find /var/www/site1.com /var/www/site2.com -type d -exec chmod 755 {} \;
sudo find /var/www/site1.com /var/www/site2.com -type f -exec chmod 644 {} \;
Create test files:
printf '%s\n' '<h1>site1.com works</h1>' | sudo tee /var/www/site1.com/public/index.html
printf '%s\n' '<h1>site2.com works</h1>' | sudo tee /var/www/site2.com/public/index.html
For WordPress or another PHP application, upload files into the matching public directory and ensure only directories that require uploads are writable by the web process.
Step 4: Configure Nginx server blocks
Nginx selects a server block from the requested hostname. Create one configuration file per site.
sudo nano /etc/nginx/sites-available/site1.comUse this configuration for a static or PHP site:
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/log/nginx/site1.access.log;
error_log /var/log/nginx/site1.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
Create the second block by changing the domain and path:
sudo cp /etc/nginx/sites-available/site1.com /etc/nginx/sites-available/site2.com
sudo sed -i 's/site1\.com/site2.com/g; s#/var/www/site1.com#/var/www/site2.com#g; s#site1#site2#g' /etc/nginx/sites-available/site2.com
sudo ln -s /etc/nginx/sites-available/site1.com /etc/nginx/sites-enabled/site1.com
sudo ln -s /etc/nginx/sites-available/site2.com /etc/nginx/sites-enabled/site2.com
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
The try_files rule is suitable for many front-controller applications. A purely static site can use try_files $uri $uri/ =404; instead, which avoids sending unknown paths to PHP.
Step 5: Point DNS records to the dedicated server
At your DNS provider, create these records:
- An
Arecord forsite1.compointing to the server IPv4 address. - An
Arecord forwww.site1.compointing to the same address, or a CNAME tosite1.com. - Equivalent records for
site2.comandwww.site2.com. - An
AAAArecord only if IPv6 is configured and tested on the server.
Verify DNS before requesting certificates:
sudo apt install -y dnsutils
dig +short site1.com A
dig +short site2.com A
curl -I http://site1.com
DNS changes can be delayed by cached TTL values. The response from dig must contain the server address before continuing.
Step 6: Enable HTTPS with Let's Encrypt
Install Certbot and request one certificate covering both the apex and www hostname for each site.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d site1.com -d www.site1.com
sudo certbot --nginx -d site2.com -d www.site2.com
Choose the redirect option when Certbot asks whether HTTP should redirect to HTTPS. Test renewal without changing live certificates:
sudo certbot renew --dry-run
sudo nginx -t
curl -I https://site1.com
curl -I https://site2.com
Keep ports 80 and 443 open because HTTP-01 certificate renewal needs port 80 unless you use DNS validation.
Step 7: Add PHP-FPM isolation and database users
For several independent PHP applications, separate PHP-FPM pools reduce cross-site access and make resource limits easier to apply. Create a pool for site1:
sudo nano /etc/php/8.3/fpm/pool.d/site1.conf[site1]
user = site1
group = site1
listen = /run/php/site1.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 12
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
php_admin_value[open_basedir] = /var/www/site1.com:/tmp
Create the system user and repeat the pool with site2-specific values:
sudo useradd --system --home /var/www/site1.com --shell /usr/sbin/nologin site1
sudo chown -R site1:site1 /var/www/site1.com
sudo systemctl restart php8.3-fpm
sudo systemctl status php8.3-fpm --no-pager
Change site1's Nginx PHP line to fastcgi_pass unix:/run/php/site1.sock;. Do not set pm.max_children too high: a practical starting point is 2 to 4 workers per GB of RAM available to PHP, then adjust after measuring worker memory use.
Create separate MariaDB credentials rather than using root:
sudo mariadbCREATE DATABASE site1db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site1user'@'localhost' IDENTIFIED BY 'CHANGE_THIS_LONG_PASSWORD';
GRANT ALL PRIVILEGES ON site1db.* TO 'site1user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Step 8: Test every website and monitor logs
Check configuration syntax, HTTP status, virtual-host routing, PHP execution, and certificate renewal:
sudo nginx -t
curl -I https://site1.com
curl -I https://site2.com
curl -s https://site1.com | head
sudo tail -f /var/log/nginx/site1.access.log /var/log/nginx/site1.error.log
Test hostname routing directly against the server address if DNS is not ready:
curl --resolve site1.com:80:SERVER_IP http://site1.com
curl --resolve site2.com:80:SERVER_IP http://site2.com
Monitor resource pressure with:
free -h
uptime
df -h
sudo systemctl --failed
sudo journalctl -u nginx -u php8.3-fpm --since '1 hour ago'
Set log rotation, collect metrics for CPU, RAM, disk latency, disk usage, HTTP status codes, and certificate expiry, and keep at least one backup outside the server. A database dump can be created with:
sudo mysqldump --single-transaction site1db | gzip | sudo tee /var/backups/site1db-$(date +%F).sql.gz > /dev/nullTroubleshooting common problems
Every domain displays the same website
Check that each server_name is correct, the site is enabled, and the default block is removed. Run sudo nginx -T and search for duplicate hostnames.
Nginx returns 403 Forbidden
Confirm that the document root exists, directories have execute permission, and the Nginx user can traverse the path:
namei -l /var/www/site1.com/public
sudo tail -n 50 /var/log/nginx/site1.error.logPHP returns 502 Bad Gateway
The PHP-FPM socket may be wrong or the service may be stopped. Run:
ls -l /run/php/
sudo systemctl status php8.3-fpm
sudo journalctl -u php8.3-fpm -n 50 --no-pagerCertbot cannot validate a domain
Check that DNS resolves to this server, port 80 is reachable, and Nginx loads the correct server block. A proxy or CDN must also forward HTTP validation requests correctly.
One site consumes all resources
Inspect access logs and PHP-FPM workers, enable application caching, limit upload sizes, and lower that site's pool limits. For noisy neighbors or compliance requirements, move the application to a separate VPS or dedicated server.
Operational recommendations
- Use version-controlled deployment files and test Nginx changes with
nginx -tbefore every reload. - Keep applications, plugins, Ubuntu packages, and PHP updated.
- Use separate Unix users, databases, logs, and deployment directories for each site.
- Enable automated backups and periodically perform a full restore test.
- Use a CDN or object storage for large static assets and video rather than consuming all server bandwidth.
- For mail, use a separate mail-focused service or server when deliverability and reputation are business-critical.