
Nginx is the web server of choice for VPS deployments — it handles high concurrency with low memory usage. This guide covers a complete production-ready Nginx setup on Ubuntu, including PHP-FPM server blocks, WordPress permalink support, gzip compression, browser cache headers, and free SSL via Let's Encrypt.
Install Nginx and PHP-FPM
sudo apt update
sudo apt install -y nginx php8.3-fpm php8.3-mysql php8.3-curl \
php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip php8.3-redis
sudo systemctl enable nginx php8.3-fpm
sudo systemctl start nginx php8.3-fpm
Server Block for a Generic PHP Site
sudo nano /etc/nginx/sites-available/mysite.com
server {
listen 80;
server_name mysite.com www.mysite.com;
root /var/www/mysite.com/public;
index index.php index.html;
access_log /var/log/nginx/mysite.com.access.log;
error_log /var/log/nginx/mysite.com.error.log;
# PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
# Block .htaccess
location ~ /\.ht { deny all; }
# Browser cache for static assets
location ~* \.(jpg|jpeg|png|webp|gif|ico|css|js|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
sudo ln -s /etc/nginx/sites-available/mysite.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Server Block for WordPress
server {
listen 80;
server_name wordpress.example.com;
root /var/www/wordpress;
index index.php;
# WordPress permalink support
location / {
try_files $uri $uri/ /index.php?$args;
}
# Deny PHP execution in uploads
location ~* /(?:uploads|files)/.*\.php$ { deny all; }
location ~* \.(txt|log|conf|bak)$ { deny all; }
# PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
# Long-lived cache for static assets
location ~* \.(css|js|jpg|jpeg|png|webp|gif|ico|svg|woff2|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
log_not_found off;
}
}
Understanding the Server Block Line by Line
The heart of Nginx configuration on a VPS is the server block (the equivalent of Apache's VirtualHost). Each directive in this block has a specific job, and understanding what every line does lets you troubleshoot faster and tune precisely instead of blindly copy-pasting. The table below explains the core directives used for PHP and WordPress sites.
| Directive | What it does |
|---|---|
listen 80; | Tells Nginx to accept requests on port 80 (HTTP); Certbot later adds port 443 (HTTPS) |
server_name | Specifies which domains this block answers for — list the main domain, www, or multiple domains separated by spaces |
root | The actual folder on disk holding your web files, e.g. /var/www/mysite.com/public |
index | Default file when a folder is requested — for PHP, index.php must come first |
try_files | Tries files in order, falling back to index.php if none exist — the core of pretty URLs |
fastcgi_pass | Hands .php files off to PHP-FPM for processing over a Unix socket |
When a request arrives, Nginx finds the block whose server_name matches, then uses root as the base for locating files. Static files (images, CSS, JS) are served directly, while .php files are passed to PHP-FPM via fastcgi_pass and the result is returned to the user. This separation of duties is exactly why Nginx serves static assets so fast — it never wakes up PHP unless it has to.
Configure WordPress Permalinks and Pretty URLs on Nginx
The most common problem when moving WordPress to Nginx is that the home page works but every other page returns 404. The cause: Nginx has no .htaccess like Apache, so it doesn't understand "Post name" permalinks (such as /category/my-post/). You have to tell Nginx yourself, using try_files to send requests that don't map to a real file over to index.php so WordPress can handle routing.
# The block that makes permalinks work
location / {
try_files $uri $uri/ /index.php?$args;
}
This means: try the exact URI first ($uri), then as a directory ($uri/), and if still not found, send everything to index.php with the original query string (?$args). Then go to WordPress > Settings > Permalinks, choose "Post name", and click Save — no extra rewrite file needed.
If you run WordPress Multisite (subdirectory), add a special rule to support the sub-site structure:
# For WordPress Multisite (subdirectory)
if (!-e $request_filename) {
rewrite /wp-admin$ $scheme://$host$uri/ permanent;
rewrite ^(/[^/]+)?(/wp-.*) $2 last;
rewrite ^(/[^/]+)?(/.*\.php) $2 last;
}
After every config change, always run sudo nginx -t to validate syntax before reloading — otherwise a broken config reloaded live will take the whole site down.
Enable Gzip Compression
Add inside the http { } block in /etc/nginx/nginx.conf:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
Enabling gzip shrinks HTML, CSS, and JavaScript by 60-80% before they reach the browser, making pages load noticeably faster — especially on mobile networks. A gzip_comp_level of 6 is the sweet spot between compression ratio and CPU cost; going higher uses more CPU for diminishing size gains. The gzip_min_length 256 setting stops Nginx from wasting effort compressing tiny files where it isn't worth it.
Enable Browser Cache and Set client_max_body_size
Beyond gzip, two settings you can't skip on a WordPress VPS are cache headers for static files and raising the maximum upload size. By default Nginx limits the request body to just 1 MB, which prevents uploading large images or plugins and triggers the 413 Request Entity Too Large error.
# Set in server { } or http { }
# Raise max upload size to 64 MB
client_max_body_size 64M;
# Browser cache for static files (1-year client-side cache)
location ~* \.(css|js|jpg|jpeg|png|webp|gif|ico|svg|woff2|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
The expires 1y paired with Cache-Control "public, immutable" tells browsers to keep static files locally for a year, so returning visitors don't re-download them — reducing server load and making pages snap into view. For WordPress, set client_max_body_size to match upload_max_filesize and post_max_size in php.ini; otherwise whichever value is smaller becomes the real limit.
Good to know: If you want full-page caching to make WordPress as fast as a static site, you can use Nginx FastCGI cache with fastcgi_cache_path. For most sites, however, browser cache plus a caching plugin like WP Super Cache is plenty.
Add SSL with Let's Encrypt (Certbot)
# Install certbot
sudo apt install -y certbot python3-certbot-nginx
# Obtain certificate (Certbot auto-edits your Nginx config)
sudo certbot --nginx -d mysite.com -d www.mysite.com
# Verify auto-renewal timer
sudo systemctl status certbot.timer
Verify SSL: After installation, run sudo nginx -t then test the certificate at the SSL Checker tool to confirm everything is correct.
Security Headers
Add to your server block to improve security scores:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Tune PHP-FPM Pool
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
; Tune based on available RAM
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500
sudo systemctl restart php8.3-fpm
Rule of Thumb: Set pm.max_children to roughly (Total RAM - 512 MB for OS) / RAM per PHP process. Each PHP-FPM worker typically uses 30-50 MB, so a 2 GB VPS can safely handle 20-30 children.
Test the Config with nginx -t, Reload, and Common Issues
Before every reload, always test the config first with nginx -t, which validates all syntax without touching the running site. On success it prints syntax is ok and test is successful; only then should you reload. Unlike a restart, a reload loads the new config without dropping in-flight connections, so the site never goes down for even a second.
# 1. Test the syntax of all config files
sudo nginx -t
# 2. If it passes, reload without dropping connections
sudo systemctl reload nginx
# 3. Watch the error log in real time when debugging
sudo tail -f /var/log/nginx/error.log
# 4. Check service status
sudo systemctl status nginx php8.3-fpm
When configuring Nginx on a real VPS you'll run into the same handful of problems again and again. The table below lists the most common errors with targeted fixes:
| Symptom / Error | Cause and fix |
|---|---|
| 502 Bad Gateway | PHP-FPM is down or the socket path is wrong. Verify fastcgi_pass matches the real socket (php8.3-fpm.sock) and run systemctl status php8.3-fpm |
| 404 on every page but home | Missing try_files $uri $uri/ /index.php?$args; in the WordPress location / block |
| 413 Request Entity Too Large | Upload exceeds the default 1 MB. Add client_max_body_size 64M; |
| 403 Forbidden | Wrong file permissions. Set owner to www-data, folders 755, files 644 |
| .php downloads instead of running | Missing location ~ \.php$ block or PHP-FPM not installed. Check the config and install php-fpm |
The best troubleshooting habit is to read the error log first, always. Nginx records the real cause in /var/log/nginx/error.log and PHP-FPM in /var/log/php8.3-fpm.log. Guessing without checking the log usually wastes time. With full root access on a VPS you can reach these logs freely — unlike shared hosting, where they're often locked away.
Frequently Asked Questions
What's the difference between Nginx and Apache, and which should I use on a VPS?
Nginx uses an event-driven architecture that handles many concurrent connections with low memory use, making it ideal for resource-limited VPS instances and high-traffic sites. Apache is more flexible with .htaccess and modules but uses more RAM as connections grow. For a VPS running PHP/WordPress we recommend Nginx + PHP-FPM for its better performance-per-RAM.
Do I need to install PHP-FPM separately?
Yes. Nginx doesn't process PHP itself the way Apache does through mod_php, so you install php-fpm separately and let Nginx pass .php files to it via fastcgi_pass. This separation is actually an advantage — you can tune the PHP-FPM pool independently and manage resources more effectively.
Why doesn't my site change after editing the config?
The most common reason is that you haven't reloaded Nginx. Always run sudo nginx -t && sudo systemctl reload nginx. The other culprit is the browser caching old files — try Incognito mode or clear the cache. And if you use a WordPress caching plugin, remember to purge its cache too.
How big a VPS do I need for WordPress + Nginx?
A typical WordPress site with moderate traffic runs comfortably on a 2 GB RAM VPS or larger, since Nginx + PHP-FPM use memory efficiently. If you're just starting out or running a small site, AsiaGB's Ubuntu VPS from 500 THB/month with full root access is enough to install Nginx, PHP-FPM, and WordPress in full.
Need a VPS for Full Nginx Control?
Linux VPS starting at 500 THB/month with Full Root Access — configure Nginx, PHP-FPM, WordPress, and any web stack exactly as you need.
View VPS Plans