
Nginx (pronounced "engine-x") is one of the world's most widely deployed web servers, trusted by high-traffic sites like Netflix, Airbnb, and GitHub. Its event-driven, non-blocking architecture allows a single worker process to handle tens of thousands of concurrent connections without excessive resource usage. This guide walks you through installing Nginx on an Ubuntu 22.04 VPS from scratch — covering static file serving, virtual hosts, reverse proxy for Node.js, SSL with Let's Encrypt, Gzip compression, and essential security headers.
What Is Nginx and How Does It Differ From Apache?
Apache and Nginx are both popular web servers, but they handle connections in fundamentally different ways.
Apache: Process-based / Thread-based
Apache uses a process-based (or thread-based) concurrency model. For each incoming request, Apache spawns a new process or thread. Under high concurrency — say 1,000 simultaneous connections — Apache creates 1,000 processes or threads, consuming significant RAM and CPU. This model works well for dynamic content and .htaccess flexibility, but it does not scale as efficiently under load.
Nginx: Event-driven, Non-blocking
Nginx uses an event-driven, non-blocking I/O model. A single worker process can manage thousands of connections simultaneously by queuing events rather than blocking on each one. This results in significantly lower memory usage compared to Apache under the same traffic load.
- Apache: Best for sites that depend on .htaccess, or need a wide range of modules (mod_rewrite, mod_php).
- Nginx: Best for static file serving, reverse proxy in front of Node.js/Python/PHP-FPM, and high-traffic sites that require maximum performance.
- Memory: Under high concurrency, Nginx typically uses 2–5x less RAM than Apache.
- Static files: Nginx serves static assets (HTML, CSS, JS, images) noticeably faster than Apache out of the box.
Which should you choose? If you run Node.js, Python (Django/Flask), or PHP-FPM on a VPS and need a lightweight reverse proxy, Nginx is the better fit. If you rely on cPanel or need per-directory .htaccess overrides, Apache may be more convenient.
Installing Nginx on Ubuntu 22.04
Connect to your VPS via SSH, then run the following commands to install and start Nginx.
Step 1: Update Package Index and Install
apt update && apt install nginx -y
Step 2: Enable and Start the Service
systemctl enable nginx systemctl start nginx
The enable command ensures Nginx starts automatically on server reboot. The start command launches it immediately.
Step 3: Verify the Status and Port 80
# Check that Nginx is running systemctl status nginx # Confirm Port 80 is listening ss -tlnp | grep :80 # If using UFW, open HTTP and HTTPS ufw allow 'Nginx Full' ufw status
Open a browser and navigate to your VPS IP address, e.g. http://123.456.789.0. You should see the "Welcome to nginx!" page, confirming a successful installation.
Nginx File Structure Overview
Understanding the file layout before making changes will save you a lot of debugging time.
/etc/nginx/ ├── nginx.conf # Main config file (global settings) ├── conf.d/ # Auto-included configs (custom global settings) ├── sites-available/ # All virtual host definitions (stored but not yet active) │ └── default # Default virtual host ├── sites-enabled/ # Symlinks to active virtual hosts │ └── default -> ../sites-available/default ├── modules-available/ # Available modules ├── modules-enabled/ # Loaded modules └── snippets/ # Reusable config fragments (e.g. SSL params)
- nginx.conf: Sets worker_processes, events block, and include paths. Rarely needs heavy editing.
- sites-available/: Store all your virtual host configs here. Files here are not active until symlinked.
- sites-enabled/: Nginx only loads configs from this directory. Files here are symlinks pointing to sites-available.
Configuring a Virtual Host for a Static Website
In Nginx terminology, a "virtual host" is called a server block. It tells Nginx which domain to respond to, where to find the files, and how to handle requests.
Create the Document Root Directory
# Create the document root mkdir -p /var/www/mysite.com/html # Set ownership to www-data (the user Nginx runs as) chown -R www-data:www-data /var/www/mysite.com # Set appropriate permissions chmod -R 755 /var/www/mysite.com
Create the Server Block Config File
nano /etc/nginx/sites-available/mysite.conf
Add the following content, replacing mysite.com with your domain.
server {
listen 80;
listen [::]:80;
server_name mysite.com www.mysite.com;
root /var/www/mysite.com/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Block access to hidden files (e.g. .env, .git)
location ~ /\. {
deny all;
}
# Log files
access_log /var/log/nginx/mysite.com.access.log;
error_log /var/log/nginx/mysite.com.error.log;
}Enable the Virtual Host and Reload
# Create a symlink from sites-available to sites-enabled ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/ # Test the config for syntax errors (very important!) nginx -t # If the test passes, reload gracefully (no downtime) systemctl reload nginx
Important: Always run nginx -t before systemctl reload nginx. If a config file has a syntax error and you reload without testing, Nginx may fail to start and take your site down.
Serving Static Files: root, index, try_files, and location Blocks
Several key directives control how Nginx serves static content. Here is an expanded example that adds caching headers for assets.
server {
listen 80;
server_name mysite.com www.mysite.com;
root /var/www/mysite.com/html;
# Files to serve when the request path is a directory
index index.html index.htm;
# Primary location: try exact file, then directory, then 404
location / {
try_files $uri $uri/ =404;
}
# Cache images and static assets for 30 days
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Block hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}- root: Sets the document root where Nginx looks for files.
- index: The file served when the path resolves to a directory.
- try_files $uri $uri/ =404: Tries to serve the exact URI as a file, then as a directory, and falls back to a 404 if neither exists.
- location ~* ...: Case-insensitive regex match for specific file types.
Nginx as a Reverse Proxy for Node.js / PHP-FPM
One of the most common Nginx use cases is acting as a reverse proxy — sitting in front of an application server (Node.js, Python, PHP-FPM) and forwarding requests to it while handling TLS termination, caching, and compression.
Reverse Proxy for Node.js
server {
listen 80;
server_name myapp.com www.myapp.com;
location / {
proxy_pass http://localhost:3000;
# Forward original headers to the application
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;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}Upstream Block for Load Balancing
If you run multiple application instances (e.g. a Node.js cluster or multiple app servers), use an upstream block to distribute traffic across them.
# Define the upstream group before the server block
upstream myapp_backend {
# Round-robin by default
server localhost:3000;
server localhost:3001;
server localhost:3002;
# Reuse connections with keepalive
keepalive 16;
}
server {
listen 80;
server_name myapp.com www.myapp.com;
location / {
proxy_pass http://myapp_backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # Required for keepalive upstream
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;
}
}Reverse Proxy for PHP-FPM
server {
listen 80;
server_name myphp.com www.myphp.com;
root /var/www/myphp.com/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass PHP files to PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}SSL with Let's Encrypt and Certbot
Let's Encrypt is a free Certificate Authority that issues SSL certificates automatically. Certbot is the official client that integrates directly with Nginx to obtain and install certificates in one command.
Step 1: Install Certbot
apt install certbot python3-certbot-nginx -y
Step 2: Obtain a Certificate and Auto-configure Nginx
# Replace example.com with your domain # Certbot will update nginx config automatically and redirect HTTP to HTTPS certbot --nginx -d example.com -d www.example.com
Certbot will prompt for your email address (for expiry notifications) and ask whether to redirect all HTTP traffic to HTTPS. Choosing to redirect is strongly recommended.
Step 3: Verify Auto-renewal
Certbot installs a systemd timer (or cron job) that automatically renews certificates before they expire. Verify it works correctly with a dry run.
# Test renewal without actually renewing certbot renew --dry-run # Check the systemd timer status systemctl status certbot.timer # Or check the cron entry (depending on your system) cat /etc/cron.d/certbot
Let's Encrypt certificates are valid for 90 days. Certbot automatically renews them when fewer than 30 days remain.
Prerequisite: Before running Certbot, your domain's DNS A record must already point to your VPS IP address. If the domain does not resolve to the server, Certbot's domain ownership verification (ACME challenge) will fail.
Gzip Compression in Nginx
Enabling Gzip compression reduces the size of transferred files by 60–80%, significantly improving page load times for HTML, CSS, JS, and JSON responses.
Add the following to the http block in /etc/nginx/nginx.conf, or create a dedicated file at /etc/nginx/conf.d/gzip.conf.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/x-javascript
application/xml
application/xml+rss
application/xhtml+xml
image/svg+xml
font/woff
font/woff2;- gzip_comp_level 6: Compression level from 1 (fastest/least compression) to 9 (slowest/best compression). Level 6 is a good balance.
- gzip_min_length 256: Skip compression for files smaller than 256 bytes — the overhead is not worth it.
- gzip_vary on: Adds the
Vary: Accept-Encodingheader so caches handle compressed and uncompressed responses correctly.
Security Headers in Nginx
Adding security-related response headers protects against common attack vectors such as clickjacking, MIME sniffing, and information leakage. Add these inside your server block.
server {
# ... other config ...
# Prevent clickjacking (disallow embedding in iframes from other origins)
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Control the referrer sent to third-party sites
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Legacy XSS protection for older browsers
add_header X-XSS-Protection "1; mode=block" always;
# Force HTTPS for the duration of max-age (requires SSL to be working first)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Hide the Nginx version number from response headers
server_tokens off;
}Note: Only add Strict-Transport-Security (HSTS) after confirming your HTTPS setup works correctly. Once set, browsers will refuse HTTP connections for the entire max-age period — if SSL breaks, users will be locked out.
Testing Config and Reloading Safely
Nginx includes a built-in config test that should be run before every reload or restart to prevent downtime caused by configuration errors.
# Test config syntax without touching the running server nginx -t # Expected output on success: # nginx: the configuration file /etc/nginx/nginx.conf syntax is ok # nginx: configuration file /etc/nginx/nginx.conf test is successful # Graceful reload (zero downtime — serves in-flight requests until complete) systemctl reload nginx # Full restart (brief downtime — use only when necessary) systemctl restart nginx # Tail the error log in real time tail -f /var/log/nginx/error.log # Tail the access log in real time tail -f /var/log/nginx/access.log
Reload vs Restart: systemctl reload nginx performs a graceful reload — Nginx forks new worker processes with the updated config while old workers finish serving in-flight requests, resulting in zero downtime. restart stops and starts Nginx entirely, which may drop active connections.
Common Problems and How to Fix Them
502 Bad Gateway
This error means Nginx cannot reach the backend (Node.js or PHP-FPM has stopped, or the port is wrong).
# Check that the backend service is running systemctl status php8.2-fpm # Or for Node.js ps aux | grep node # Confirm the backend is listening on the expected port ss -tlnp | grep 3000 # Review Nginx error logs tail -100 /var/log/nginx/error.log # Restart the backend systemctl restart php8.2-fpm
Permission Denied (403 Forbidden)
Usually caused by incorrect file or directory permissions, or AppArmor/SELinux blocking access.
# Check ownership of files in the document root
ls -la /var/www/mysite.com/
# Fix ownership for www-data
chown -R www-data:www-data /var/www/mysite.com/
# Set correct permissions: directories 755, files 644
find /var/www/mysite.com/ -type d -exec chmod 755 {} \;
find /var/www/mysite.com/ -type f -exec chmod 644 {} \;Config Syntax Error
Happens after a bad edit. nginx -t will fail and Nginx will refuse to reload.
# Run to see the exact error and line number nginx -t # Common error messages: # nginx: [emerg] unexpected "}" — missing semicolon on a previous line # nginx: [emerg] "server" directive is not allowed here — block is in the wrong place # nginx: [emerg] host not found in upstream — upstream name or port is wrong # If Nginx stopped entirely, restore the last known-good config # then fix the error and start again
Nginx Shows Default Page Instead of Your Site
# Check that the symlink exists in sites-enabled ls -la /etc/nginx/sites-enabled/ # Verify the server_name matches your domain grep server_name /etc/nginx/sites-available/mysite.conf # Remove the default site if it is interfering rm /etc/nginx/sites-enabled/default nginx -t && systemctl reload nginx
Ready to Run Nginx on Your Own VPS?
AsiaGB VPS gives you full root access — install Nginx, Node.js, or PHP-FPM in minutes. Linux VPS plans start from 500 THB/month. Data centers in Thailand and Singapore.
View AsiaGB VPS Plans