Quick take: Nginx is the right web server for almost every modern deployment — faster than Apache for static files, and ideal as a reverse proxy in front of Node.js, Python, or PHP apps. On Ubuntu it takes under five minutes to install and a further thirty to configure SSL, server blocks, and a reverse proxy.
Introduction
Nginx powers roughly a third of all websites on the internet — everything from personal blogs to large-scale APIs handling millions of requests per hour. Its event-driven architecture handles thousands of simultaneous connections with a fraction of the memory that thread-based servers like Apache consume. On Ubuntu, it is the first thing I install on any new server before deploying an application.
This guide walks through installation, the directory structure that confuses most newcomers, creating server blocks for multiple domains, SSL with Let's Encrypt, and configuring Nginx as a reverse proxy in front of a backend application — which is the most common real-world use case.
Nginx vs Apache: Which to Choose
Apache creates a new thread or process for each incoming connection. This works well for dynamic content with built-in modules like mod_php, but the memory cost adds up under heavy concurrent load. Nginx uses a single-threaded event loop that handles thousands of connections simultaneously without proportional memory growth.
In practice: use Nginx for serving static files, acting as a reverse proxy in front of a separate app server, load balancing, or as an SSL terminator. Use Apache when you need its rich module ecosystem for legacy applications, particularly PHP applications that rely on .htaccess files. For most modern setups — a React frontend, a Node.js API, a Django app — Nginx is the right choice.
Installing Nginx on Ubuntu
Nginx is in Ubuntu's default repositories. On Ubuntu 20.04, 22.04, or 24.04:
sudo apt update
sudo apt install nginx -y
Start Nginx and enable it to start automatically on boot:
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx
Allow Nginx through the firewall. The Nginx Full profile opens both HTTP (port 80) and HTTPS (port 443):
sudo ufw allow 'Nginx Full'
sudo ufw status
Open your server's IP address in a browser. You should see the default Nginx welcome page — that confirms Nginx is installed and serving correctly.
Nginx Directory Structure Explained
Understanding where configuration files live saves hours of confusion. These are the paths that matter:
/etc/nginx/nginx.conf # Main config — global settings, includes all sites-enabled/*.conf
/etc/nginx/sites-available/ # Config files — one per site, written here but not yet active
/etc/nginx/sites-enabled/ # Symlinks to files in sites-available — only these are loaded
/var/log/nginx/access.log # All incoming requests
/var/log/nginx/error.log # All errors — first place to check when something breaks
/var/www/html/ # Default web root for the default site
The sites-available / sites-enabled separation is intentional. You write config files in sites-available and create symlinks in sites-enabled to activate them. Deactivating a site is as simple as removing the symlink — the config stays intact and can be re-enabled without rewriting anything.
Configuring a Server Block (Virtual Host)
A server block tells Nginx which domain to handle and where to find its files. Start by removing the default site so it does not interfere:
sudo rm /etc/nginx/sites-enabled/default
Create a config file for your site:
sudo nano /etc/nginx/sites-available/example.com
For a static site:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log;
}
Create the web root and a test page:
sudo mkdir -p /var/www/example.com
echo "<h1>It works</h1>" | sudo tee /var/www/example.com/index.html
sudo chown -R www-data:www-data /var/www/example.com
Enable the site by creating a symlink, test the config, then reload:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Always run nginx -t before reloading. It validates your configuration and reports any syntax errors. A config error without testing will take Nginx down when you reload.
Adding Free SSL with Let's Encrypt and Certbot
Certbot automates free SSL certificate issuance from Let's Encrypt and modifies your Nginx config automatically:
sudo apt install certbot python3-certbot-nginx -y
Request a certificate. Certbot will update your Nginx server block with SSL directives and add an HTTP-to-HTTPS redirect:
sudo certbot --nginx -d example.com -d www.example.com
Certbot also installs a cron job or systemd timer that renews certificates automatically before they expire. Test the renewal process to confirm it works:
sudo certbot renew --dry-run
Check your certificate status and expiry at any time:
sudo certbot certificates
Setting Up Nginx as a Reverse Proxy
The most common real-world use of Nginx on application servers: sit in front of a backend app (Node.js on port 3000, Python on port 8000, a Docker container) and proxy requests to it. The public only ever connects to Nginx on ports 80 and 443. The backend app is never directly exposed.
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_read_timeout 90;
}
}
The proxy_set_header lines are important — without them, your backend sees all requests as coming from 127.0.0.1 instead of the real client IP, which breaks rate limiting, logging, and geo-detection. The Upgrade and Connection headers enable WebSocket support for real-time applications.
After adding the config, always test and reload:
sudo nginx -t && sudo systemctl reload nginx
Performance and Security Tuning
These settings belong in the http block of /etc/nginx/nginx.conf:
worker_processes auto; # Match the number of CPU cores
events {
worker_connections 1024; # Connections per worker process
}
http {
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
keepalive_timeout 65;
client_max_body_size 20M;
server_tokens off; # Hide Nginx version from response headers
# Cache static assets aggressively
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
The server_tokens off directive stops Nginx from advertising its version number in error pages and response headers, which removes a small piece of information from potential attackers. Gzip compression reduces bandwidth usage for text-based responses by 60–80% at minimal CPU cost.
Essential Nginx Commands
sudo nginx -t # Test configuration for syntax errors
sudo systemctl reload nginx # Reload config without dropping connections
sudo systemctl restart nginx # Full restart — briefly drops all connections
sudo tail -f /var/log/nginx/error.log # Watch error log in real time
sudo nginx -V # Show compiled modules and options
sudo systemctl status nginx # Check if Nginx is running
Always use reload rather than restart when pushing config changes in production. Reload sends a signal to Nginx to re-read its config while existing connections continue to be served. Restart drops all connections immediately.
Troubleshooting Common Errors
502 Bad Gateway: Nginx cannot reach the backend application. The backend is either not running or not listening on the port specified in proxy_pass. Check with:
ss -tlnp | grep 3000 # Replace 3000 with your app's port
sudo tail -50 /var/log/nginx/error.log
504 Gateway Timeout: The backend is reachable but taking too long to respond. Increase the timeout and investigate why the application is slow:
# In your server block:
proxy_read_timeout 300;
Permission denied on static files: Nginx runs as www-data. The web root and all files must be readable by that user:
sudo chown -R www-data:www-data /var/www/yoursite
sudo find /var/www/yoursite -type d -exec chmod 755 {} \;
sudo find /var/www/yoursite -type f -exec chmod 644 {} \;
Changes not taking effect: Confirm you ran sudo nginx -t (no errors) and sudo systemctl reload nginx. Test without browser cache using curl -I http://example.com.
Final Thoughts
Nginx is one of those tools that rewards time spent understanding its configuration model. The separation between sites-available and sites-enabled, the way server blocks are selected, the proxy headers that must be set — once these click into place, managing multiple sites and applications on a single server becomes straightforward and repeatable.
The configuration examples in this guide are production-ready starting points, not toy demos. The gzip settings, the proxy headers, the SSL setup with automatic renewal — I use these or close variants of them on every server I manage. Start with these, then tune for your specific application's needs.
FAQ: How to Install and Configure Nginx on Ubuntu
What is the difference between Nginx and Apache?+
Nginx uses an event-driven non-blocking architecture, making it more memory-efficient for serving many concurrent connections and static files. Apache uses a thread-per-connection model which works well for dynamic content and has a richer module ecosystem. For most modern deployments involving a separate app server, Nginx is the better choice.
How do I host multiple websites on the same Nginx server?+
Create a separate server block file in /etc/nginx/sites-available/ for each website, each with a different server_name directive. Symlink each file into sites-enabled and reload Nginx. Nginx reads the Host header from each incoming request to route it to the correct server block.
How do I fix a 502 Bad Gateway error in Nginx?+
A 502 means Nginx cannot reach your backend application. Check that the app is actually running and listening on the port specified in proxy_pass. Use ss -tlnp to list all listening ports. Then check /var/log/nginx/error.log for the specific error message.
Does Let's Encrypt SSL renew automatically?+
Yes. Certbot installs a cron job or systemd timer that runs twice daily and automatically renews any certificate expiring within 30 days. You can verify it is working correctly by running: sudo certbot renew --dry-run
How do I redirect HTTP to HTTPS in Nginx?+
Add a separate server block listening on port 80 with a 301 redirect: server { listen 80; server_name example.com; return 301 https://$host$request_uri; }. When you run Certbot with the --nginx flag, it adds this redirect automatically.
Need help with your Linux server or infrastructure?
Work directly with Muhammad Irfan Aslam for Linux, Docker, Nginx, DevOps, cloud, and server support.
Hire Me for Support