How to Install Nginx on VPS (2025) — Complete Guide
Why Nginx Is the Best Web Server for Your VPS
Nginx powers over 34% of all websites on the internet and is the default choice for high-performance web serving, reverse proxying, and load balancing. For choosing the right hosting, see Best VPS for Web Hosting. Unlike Apache's process-per-connection model, Nginx uses an event-driven, asynchronous architecture that handles thousands of concurrent connections with minimal memory overhead. A typical Nginx worker process consumes only 2-4MB of RAM, compared to Apache's 50-100MB per process. For deploying applications behind Nginx, see Deploy FastAPI on VPS and Docker on Ubuntu VPS.
This matters on a VPS where resources are finite. On a 1GB RAM instance, Apache might handle 10-20 concurrent connections before running out of memory. Nginx can handle 1,000+ concurrent connections on the same hardware. For containerized setups, check out Docker on Ubuntu VPS. This efficiency makes Nginx the ideal web server for VPS deployments, whether you are hosting static websites, API backends, or full-stack applications.
Nginx also excels as a reverse proxy. It can terminate SSL/TLS connections, distribute traffic across multiple backend servers, cache responses, and rewrite URLs — all with extremely low latency. For European hosting locations with low latency, see Germany VPS and Netherlands VPS. This guide covers everything from a basic installation to production-ready configuration with SSL, security headers, Gzip compression, reverse proxying, and performance tuning.
Prerequisites
| Requirement | Details |
|---|---|
| Operating System | Ubuntu 22.04 LTS or 24.04 LTS |
| Root Access | SSH access with root privileges or a sudo user |
| RAM | Minimum 512MB (Nginx uses under 10MB at idle) |
| Domain Name | Required for SSL certificate (point A record to VPS IP) |
| Firewall | UFW recommended (configured in this guide) |
Step 1: Update Your System
Before installing any software, update the package index and upgrade all installed packages to their latest versions. This ensures compatibility and patches known security vulnerabilities.
ssh root@your-vps-ip
apt update && apt upgrade -y
Verify your Ubuntu version to confirm compatibility.
lsb_release -a
This guide is tested on Ubuntu 22.04 LTS (Jammy Jellyfish) and Ubuntu 24.04 LTS (Noble Numbat). Both versions ship Nginx 1.18 or later from the default repositories, which includes all features used in this guide.
Step 2: Install Nginx
Install Nginx from the Ubuntu package repositories. The version in the default repositories is stable and well-tested for production use.
apt install nginx -y
After installation, Nginx starts automatically. Verify the service status.
systemctl status nginx
The output should show "active (running)." Check the Nginx version to confirm the installation.
nginx -v
Test that Nginx is serving content by opening your browser and navigating to http://your-vps-ip. You should see the default Nginx welcome page that reads "Welcome to nginx!" If you see this page, Nginx is correctly installed and running.
Note: If the welcome page does not load, check that your VPS firewall allows inbound traffic on port 80. Many VPS providers block all ports by default. See Step 3 for firewall configuration.
Step 3: Configure the Firewall (UFW)
UFW (Uncomplicated Firewall) is the default firewall management tool on Ubuntu. Nginx profiles for UFW are created automatically during installation. These profiles simplify the process of allowing the correct ports.
List the available Nginx profiles.
ufw app list
You will see three Nginx profiles:
- Nginx Full: Opens both port 80 (HTTP) and port 443 (HTTPS)
- Nginx HTTP: Opens only port 80 (HTTP)
- Nginx HTTPS: Opens only port 443 (HTTPS)
Enable the "Nginx Full" profile to allow both HTTP and HTTPS traffic.
ufw allow 'Nginx Full'
ufw allow OpenSSH
ufw --force enable
Warning: Always allow OpenSSH before enabling the firewall. If you enable UFW without allowing SSH, you will be locked out of your VPS. The --force flag is needed when running non-interactively.
Verify the firewall rules.
ufw status
You should see Nginx Full and OpenSSH listed as allowed. The firewall now permits HTTP, HTTPS, and SSH connections while blocking all other inbound traffic.
Step 4: Set Up Server Blocks (Virtual Hosts)
Server blocks (called Virtual Hosts in Apache) allow you to host multiple websites on a single VPS. Each server block defines a separate configuration for a domain, including its own document root, logs, and settings.
Create the Directory Structure
Create a directory for your website's public files. Replace example.com with your actual domain name.
mkdir -p /var/www/example.com/public_html
chown -R $USER:$USER /var/www/example.com/public_html
chmod -R 755 /var/www/example.com
Create a test HTML file to verify the server block works.
cat > /var/www/example.com/public_html/index.html << 'EOF'
Example.com
Welcome to example.com!
This server block is working correctly.
EOF
Create the Server Block Configuration
cat > /etc/nginx/sites-available/example.com << 'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public_html;
index index.html index.htm;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; allow all; }
location ~* \.(css|gif|ico|jpeg|jpg|js|png|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
EOF
Enable the Server Block
Create a symbolic link from sites-available to sites-enabled to activate the server block.
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
Test the Nginx configuration for syntax errors.
nginx -t
If the test passes (shows "syntax is ok" and "test is successful"), reload Nginx to apply the changes.
systemctl reload nginx
Navigate to http://example.com in your browser. You should see the test page you created. Repeat this process for each additional domain you want to host on the VPS.
Step 5: Install SSL with Certbot
SSL/TLS encryption is mandatory for any website in 2025. Certbot is a free tool that automates the process of obtaining and renewing Let's Encrypt certificates. The certificates are valid for 90 days and Certbot handles automatic renewal.
Install Certbot
apt install certbot python3-certbot-nginx -y
Obtain an SSL Certificate
Run Certbot with the Nginx plugin. Certbot will automatically modify your server block configuration to redirect HTTP to HTTPS and add the SSL certificate directives.
certbot --nginx -d example.com -d www.example.com
Certbot will ask you a few questions:
- Email address: Used for renewal notifications and security alerts
- Terms of Service: Agree to the Let's Encrypt Subscriber Agreement
- HTTP to HTTPS redirect: Select option 2 to redirect all HTTP traffic to HTTPS
Verify Automatic Renewal
Certbot installs a systemd timer for automatic renewal. Verify it is active.
systemctl status certbot.timer
Test the renewal process manually to ensure it works without errors.
certbot renew --dry-run
If the dry run completes without errors, your certificates will renew automatically every 60 days (Certbot renews when the certificate has 30 days remaining).
Verify SSL Configuration
After Certbot modifies your configuration, the server block at /etc/nginx/sites-available/example.com will include SSL directives. Verify the configuration looks correct.
nginx -t
Test your SSL configuration with an external tool by visiting https://www.ssllabs.com/ssltest/analyze.html?d=example.com. You should achieve an A rating.
Step 6: Enable Gzip Compression
Gzip compression reduces the size of text-based responses (HTML, CSS, JavaScript, JSON, XML) by 60-80%. This dramatically reduces bandwidth usage and improves page load times, especially for users on slower connections.
Create a Gzip configuration file.
cat > /etc/nginx/conf.d/gzip.conf << 'EOF'
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/opentype
font/ttf
font/woff
font/woff2;
EOF
The key settings explained:
- gzip on: Enables Gzip compression
- gzip_vary on: Adds a Vary: Accept-Encoding header so CDNs cache compressed and uncompressed versions separately
- gzip_comp_level 4: Compression level 4 provides a good balance between compression ratio and CPU usage. Higher levels (up to 9) use more CPU for marginal gains
- gzip_min_length 256: Only compress responses larger than 256 bytes. Compressing tiny responses wastes CPU
- gzip_types: Specifies which MIME types to compress. Do not compress images (JPEG, PNG, GIF) or binary files — they are already compressed and Gzip will increase their size
Test and reload Nginx.
nginx -t && systemctl reload nginx
Verify Gzip is working by sending a request with the Accept-Encoding header.
curl -H "Accept-Encoding: gzip" -I https://example.com
The response should include Content-Encoding: gzip and the Vary: Accept-Encoding header.
Step 7: Configure Security Headers
Security headers instruct the browser to enable built-in protections against common attacks. Create a dedicated file for security headers and include it in your server blocks.
cat > /etc/nginx/snippets/security-headers.conf << 'EOF'
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Enable XSS protection in older browsers
add_header X-XSS-Protection "1; mode=block" always;
# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Security Policy (customize for your application)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';" always;
# Permissions Policy (disable features you don't need)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Strict Transport Security (HSTS) — only enable after verifying SSL works
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
EOF
Include this file in your server block by adding the following line inside the server { } block in /etc/nginx/sites-available/example.com:
include /etc/nginx/snippets/security-headers.conf;
Warning: The HSTS header (Strict-Transport-Security) tells browsers to only connect to your site over HTTPS for the specified duration. Start with a short max-age (like 300 seconds) to test, then increase to 31536000 (one year) after confirming everything works. The preload directive submits your domain to the HSTS preload list — this is irreversible, so only add it after thorough testing.
Test and reload.
nginx -t && systemctl reload nginx
Verify the headers are being sent.
curl -I https://example.com
Step 8: Set Up a Reverse Proxy
Nginx is widely used as a reverse proxy to forward requests to backend applications (Node.js, Python, Java, etc.). The backend runs on a local port, and Nginx handles SSL termination, static file serving, and request routing.
Here is a complete reverse proxy configuration example that forwards requests to a Node.js application running on port 3000.
cat > /etc/nginx/snippets/reverse-proxy.conf << 'EOF'
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
EOF
Add a location block to your server block that proxies requests to the backend.
# Inside your server block in /etc/nginx/sites-available/example.com:
location /api/ {
include /etc/nginx/snippets/reverse-proxy.conf;
proxy_pass http://127.0.0.1:3000/;
}
# Serve static files directly without hitting the backend
location /static/ {
alias /var/www/example.com/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# WebSocket support
location /ws/ {
include /etc/nginx/snippets/reverse-proxy.conf;
proxy_pass http://127.0.0.1:3000/ws/;
proxy_read_timeout 86400;
}
EOF
Key reverse proxy settings explained:
- proxy_set_header: Passes the original client information (IP, protocol, host) to the backend so your application can see the real client IP address
- proxy_http_version 1.1: Required for keepalive connections and WebSocket support
- proxy_buffering on: Buffers responses from the backend before sending them to the client. This improves performance but may cause delays for streaming responses. Set to
offfor Server-Sent Events (SSE) or long-polling APIs - proxy_read_timeout 86400: Prevents Nginx from closing WebSocket connections after the default 60-second timeout
Test and reload.
nginx -t && systemctl reload nginx
Step 9: Performance Tuning
Optimizing Nginx for production involves tuning worker processes, connection limits, and buffering. These settings are configured in the main Nginx configuration file.
Worker Processes and Connections
cat > /etc/nginx/conf.d/performance.conf << 'EOF'
# Set worker processes to auto (matches number of CPU cores)
worker_processes auto;
# Increase the maximum number of simultaneous connections per worker
events {
worker_connections 2048;
multi_accept on;
}
# Optimize file handling
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Timeouts
keepalive_timeout 65;
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
# Buffer sizes
client_body_buffer_size 16K;
client_header_buffer_size 1k;
client_max_body_size 20m;
large_client_header_buffers 4 8k;
# Open file cache
open_file_cache max=2000 inactive=20s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Reduce logging for static assets
map $uri $loggable {
~*\.(css|gif|ico|jpeg|jpg|js|png|svg|woff|woff2|ttf|eot|mp4|webm)$ 0;
default 1;
}
access_log /var/log/nginx/access.log combined if=$loggable;
}
EOF
Note: If you already have an events { } or http { } block in your nginx.conf, you should merge these settings into that file instead of creating a separate file. The events and http blocks can only appear once in the Nginx configuration.
Understanding the Tuning Parameters
- worker_processes auto: Automatically detects the number of CPU cores and creates one worker per core. This is the recommended setting for most VPS instances
- worker_connections 2048: Each worker can handle up to 2,048 simultaneous connections. With 2 workers (2-core VPS), the maximum is 4,096 concurrent connections
- multi_accept on: Allows each worker to accept all new connections at once rather than one at a time, reducing latency under high load
- sendfile on: Uses the kernel's sendfile() system call for serving static files, which is faster than reading and writing through user space
- tcp_nopush on: Optimizes the way data is sent over the network by combining small packets into larger ones before sending
- tcp_nodelay on: Disables Nagle's algorithm for keepalive connections, ensuring data is sent immediately without waiting for more data to accumulate
- client_max_body_size 20m: Limits the maximum upload size. Adjust this based on your application needs (increase for file upload services)
- open_file_cache: Caches file descriptors for frequently accessed static files, reducing disk I/O for repeated requests
Test and reload.
nginx -t && systemctl reload nginx
Step 10: Monitor with stub_status
The Nginx stub_status module provides real-time metrics about active connections, accepted connections, handled connections, and total requests. This data is useful for monitoring and capacity planning.
Create a Status Endpoint
cat > /etc/nginx/sites-available/nginx-status << 'EOF'
server {
listen 127.0.0.1:8080;
server_name _;
location /status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
EOF
ln -s /etc/nginx/sites-available/nginx-status /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Access the status page.
curl http://127.0.0.1:8080/status
You will see output like this:
Active connections: 5
server accepts handled requests
1234 1234 5678
Reading: 0 Writing: 2 Waiting: 3
Understanding the Metrics
- Active connections: Current number of open connections, including those waiting
- accepts: Total connections accepted since Nginx started
- handled: Total connections handled (should equal accepts unless resource limits were hit)
- requests: Total requests processed since Nginx started
- Reading: Number of connections currently reading a request header
- Writing: Number of connections currently writing a response body
- Waiting: Number of idle keepalive connections waiting for a new request
For production monitoring, integrate the stub_status endpoint with Prometheus using the nginx-prometheus-exporter, or use a simple cron script that logs the metrics to a file for trend analysis.
Complete Production Server Block Example
Here is a full production-ready server block that combines all the configurations from this guide.
cat > /etc/nginx/sites-available/example.com << 'EOF'
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Security headers
include /etc/nginx/snippets/security-headers.conf;
# Document root
root /var/www/example.com/public_html;
index index.html index.htm;
# Logging
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
# Static files with caching
location /static/ {
alias /var/www/example.com/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Reverse proxy to backend
location /api/ {
include /etc/nginx/snippets/reverse-proxy.conf;
proxy_pass http://127.0.0.1:3000/;
}
# WebSocket support
location /ws/ {
include /etc/nginx/snippets/reverse-proxy.conf;
proxy_pass http://127.0.0.1:3000/ws/;
proxy_read_timeout 86400;
}
# Default location
location / {
try_files $uri $uri/ =404;
}
# Block access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
EOF
Troubleshooting Common Nginx Issues
1. "502 Bad Gateway" Error
Cause: Nginx cannot connect to the backend application. The backend may be down, listening on a different port, or binding to a different address.
Solution: Verify the backend is running with systemctl status your-app or ss -tlnp | grep 3000. Check the proxy_pass directive in your Nginx config matches the backend's actual address and port. Review the error log at /var/log/nginx/example.com.error.log for detailed error messages.
2. "403 Forbidden" Error
Cause: Nginx does not have permission to read the files in the document root, or no index file exists.
Solution: Verify file permissions with ls -la /var/www/example.com/public_html/. The directory should be owned by www-data or have at least 755 permissions. Ensure an index.html or index.php file exists. Check that the root directive in your server block points to the correct directory.
3. Nginx Fails to Start After Config Changes
Cause: Syntax error in the configuration file, a missing include file, or a port conflict.
Solution: Always run nginx -t before reloading. Check for syntax errors in the specific file. If another service is using port 80 or 443, identify it with ss -tlnp | grep -E ':(80|443)' and stop or reconfigure the conflicting service.
4. SSL Certificate Renewal Fails
Cause: Certbot cannot verify domain ownership, usually because port 80 is blocked or the domain does not resolve to the VPS IP.
Solution: Verify the domain's A record points to your VPS IP with dig example.com +short. Ensure port 80 is open in your firewall. Check the Certbot renewal logs at /var/log/letsencrypt/letsencrypt.log.
Frequently Asked Questions
Is Nginx free to use on a VPS?
Yes. Nginx is open-source software released under the BSD 2-Clause license. There are no licensing fees. The open-source version includes all features needed for web serving, reverse proxying, load balancing, and caching.
How much RAM does Nginx use?
A single Nginx worker process uses approximately 2-4MB of RAM at idle. With 2-4 workers (matching your CPU cores), total Nginx memory usage is typically under 20MB, even under moderate load. This makes Nginx ideal for VPS instances with limited RAM.
Can I host multiple domains on one VPS with Nginx?
Yes. Nginx server blocks allow you to host an unlimited number of domains on a single VPS. Each domain gets its own configuration file, document root, and log files. The only practical limits are disk space and memory for your applications.
How do I redirect www to non-www (or vice versa)?
Create a separate server block that listens for the unwanted version and redirects with return 301 https://example.com$request_uri;. Search engines treat www and non-www as different domains, so a 301 redirect consolidates your SEO ranking.
What is the difference between Nginx and Apache?
Nginx uses an event-driven architecture that handles many concurrent connections with minimal memory. Apache traditionally uses a process-per-request model that consumes more memory but offers more flexibility through .htaccess files. Nginx is generally faster and more resource-efficient, making it the better choice for VPS deployments.
How do I rate limit requests with Nginx?
Add limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; in the http block and limit_req zone=api burst=20 nodelay; in the location block. This limits each IP to 10 requests per second with a burst allowance of 20.
Can Nginx load balance across multiple backend servers?
Yes. Define an upstream block with multiple server directives and use proxy_pass http://upstream_name; in your location block. Nginx supports round-robin, least connections, and IP hash load balancing algorithms out of the box.
How do I enable HTTP/2 in Nginx?
Add http2 to the listen directive: listen 443 ssl http2;. HTTP/2 requires HTTPS. Most modern browsers and servers support HTTP/2, which provides multiplexing, header compression, and server push for improved performance.