Introduction
If you have ever set up a website, deployed a web application, or rented a VPS (Virtual Private Server), you have almost certainly encountered the name Nginx (pronounced engine-x). It powers some of the busiest websites on the internet — including Netflix, Dropbox, and WordPress.com — yet many beginners find it intimidating at first glance.
This guide will change that.
By the end of this article, you will understand exactly what Nginx is, why it exists, how it works, and how to use it to serve real websites on a Linux server. No prior experience with web servers is required.
What Is Nginx?
Nginx is a web server — software that listens for requests coming from the internet and responds with web pages, files, or data.
But Nginx is more than just a web server. It is also:
- A reverse proxy — it sits in front of your applications and forwards requests to them
- A load balancer — it can distribute traffic across multiple servers
- An HTTP cache — it can store and serve cached responses to reduce server load
- An SSL terminator — it handles HTTPS encryption so your applications don't have to
Think of Nginx as the reception desk of a large office building. Every visitor (web request) walks in through the front door (port 80 or 443). The receptionist (Nginx) greets them, figures out where they need to go, and directs them to the right department — the Laravel team, the Node.js team, or the Python team. The visitor never wanders the building themselves.
Why Not Just Use Apache?
Apache is the other major web server and has been around since 1995. For years it dominated the web. So why does Nginx exist?
The answer comes down to architecture.
Apache creates a new thread or process for every incoming connection. This works fine for low traffic, but under heavy load — thousands of simultaneous connections — Apache consumes enormous amounts of RAM and CPU.
Nginx was built in 2004 specifically to solve this problem. It uses an event-driven, asynchronous architecture. Instead of one thread per connection, a single Nginx worker process can handle thousands of simultaneous connections efficiently using very little memory.
The result:
| Apache | Nginx | |
|---|---|---|
| Architecture | Thread per connection | Event-driven |
| Memory usage under load | High | Low |
| Static file serving | Good | Excellent |
| Reverse proxy | Possible | Native, highly optimised |
| Configuration style |
.htaccess per directory |
Centralised config files |
For modern application servers running Laravel, Node.js, or Python, Nginx is the industry standard choice.
How Nginx Fits Into a Real Server
Here is the architecture that professional developers use on production servers:
Internet
|
▼
[ Nginx — Port 80/443 ]
|
├──► example.com → PHP-FPM (Laravel app)
├──► api.example.com → Node.js running on port 3000
└──► app.example.com → Python running on port 8000
Notice that none of the applications talk directly to the internet. Only Nginx does. This is the reverse proxy pattern and it is fundamental to how modern servers work.
Why is this pattern so powerful?
Security: Your Node.js app running on port 3000 is never exposed to the internet. UFW (the firewall) blocks port 3000 publicly. Only Nginx — on port 443 — is reachable, and Nginx decides what to forward.
SSL in one place: Instead of configuring HTTPS in your Laravel app, your Node.js app, and your Python app separately, you configure it once in Nginx. All apps automatically get HTTPS.
Multiple apps on one server: Without a reverse proxy, you could only run one application per server (one thing can listen on port 80 at a time). With Nginx routing by domain name, you can run dozens of applications on a single server.
Static files: Nginx serves static files (images, CSS, JavaScript) directly from disk at incredible speed — without involving PHP, Node.js, or Python at all. This dramatically reduces load on your application.
Understanding Nginx Configuration
Nginx configuration lives in /etc/nginx/ on Ubuntu/Debian systems. Here is the directory layout:
/etc/nginx/
├── nginx.conf ← Main configuration file
├── sites-available/ ← All site configs (active or not)
│ ├── example.com
│ └── api.example.com
├── sites-enabled/ ← Symlinks to active sites only
│ ├── example.com → ../sites-available/example.com
│ └── api.example.com → ../sites-available/api.example.com
├── conf.d/ ← Additional configuration fragments
└── snippets/ ← Reusable configuration pieces
The sites-available and sites-enabled pattern
This is an elegant system. You write your site configuration in sites-available/. To activate it, you create a symbolic link (a shortcut) in sites-enabled/. To deactivate a site without deleting its configuration, you simply remove the symlink.
# Enable a site
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Disable a site (config is preserved)
sudo rm /etc/nginx/sites-enabled/example.com
Server Blocks — Nginx's Virtual Hosts
In Nginx, each website or application is configured using a server block. This is equivalent to Apache's Virtual Hosts. A server block tells Nginx: "When a request comes in for this domain name, here is how to handle it."
Example 1: Serving a Static HTML Website
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.html;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
Let's break this down line by line:
-
listen 80— listen for HTTP traffic on port 80 -
listen [::]:80— also listen on IPv6 -
server_name example.com www.example.com— this block handles requests for these domain names -
root /var/www/example.com/public— files are served from this directory -
index index.html— the default file to serve when a directory is requested -
try_files $uri $uri/ =404— try to find the requested file; return 404 if not found -
access_loganderror_log— where to write logs for this site
Example 2: Serving a Laravel (PHP) Application
Laravel is a PHP framework that requires PHP-FPM to process .php files. Here is how Nginx is configured to work with it:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
The key addition here is the location ~ \.php$ block. This tells Nginx: "For any request ending in .php, don't serve it as a static file — pass it to PHP-FPM for processing."
PHP-FPM (FastCGI Process Manager) is a separate service that runs PHP code and returns the result to Nginx. They communicate via a Unix socket (php8.3-fpm.sock) — a fast, secure local communication channel.
The location ~ /\.(?!well-known).* block denies access to hidden files (files starting with a dot, like .env). This is a critical security rule — your .env file contains database passwords and application secrets, and it must never be publicly accessible.
Example 3: Reverse Proxy to Node.js
When your Node.js/Express application is running on port 3000, Nginx forwards requests to it like this:
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location / {
proxy_pass http://localhost: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;
}
access_log /var/log/nginx/api.example.com.access.log;
error_log /var/log/nginx/api.example.com.error.log;
}
The proxy_pass http://localhost:3000 directive is the core of reverse proxying. Every request that hits api.example.com is forwarded to your Node.js process running locally on port 3000.
The proxy_set_header directives pass important information to your Node.js app — particularly X-Real-IP and X-Forwarded-For, which tell your application the real IP address of the visitor (since from Node.js's perspective, all requests appear to come from localhost).
Important Security Headers
Every Nginx server block should include these security headers:
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
- X-Frame-Options SAMEORIGIN — prevents your site from being embedded in an iframe on another website (protects against clickjacking attacks)
- X-Content-Type-Options nosniff — prevents browsers from guessing the content type of a response (protects against MIME type confusion attacks)
- X-XSS-Protection — enables the browser's built-in cross-site scripting filter
Hiding the Nginx Version Number
By default, Nginx tells the world exactly which version it is running. This is visible in HTTP response headers:
Server: nginx/1.24.0
This is a gift to attackers — they can look up known vulnerabilities for that exact version. One line in your nginx.conf fixes this:
server_tokens off;
After this, the header simply shows:
Server: nginx
Testing and Reloading Configuration
This is the most important operational habit with Nginx. Always test your configuration before reloading.
# Test configuration syntax
sudo nginx -t
# If the test passes, reload gracefully
sudo systemctl reload nginx
The difference between reload and restart is important:
-
reload— applies the new configuration without dropping existing connections. Zero downtime. -
restart— stops and starts Nginx completely. Active connections are dropped.
Always use reload in production. Only use restart if Nginx is genuinely broken and needs a full restart.
A passing test looks like this:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If you see any errors, fix them before reloading. A broken configuration with a running reload command will leave the old configuration running — Nginx is smart enough not to apply a broken config.
Understanding Nginx Worker Processes
When Nginx starts on a modern server, you will see multiple processes running:
nginx: master process
nginx: worker process
nginx: worker process
nginx: worker process
nginx: worker process
The master process manages the workers and handles configuration reloads. It runs as root so it can bind to ports 80 and 443.
The worker processes handle the actual connections. They run as the www-data user — a low-privilege account — so even if a worker is compromised, the damage is limited.
By default on Ubuntu 24.04, Nginx automatically sets the number of workers to match your CPU core count. On a 4-core server you get 4 workers. This is the correct production setting.
Nginx Logs
Every site configured in Nginx writes to its own log files:
# Access log — every request
/var/log/nginx/example.com.access.log
# Error log — problems and warnings
/var/log/nginx/example.com.error.log
To watch requests in real time:
sudo tail -f /var/log/nginx/example.com.access.log
To watch errors:
sudo tail -f /var/log/nginx/example.com.error.log
The access log shows you every request: the IP address, timestamp, requested URL, HTTP status code, and response size. The error log shows you configuration problems, upstream connection failures, and PHP-FPM errors.
Common Nginx Commands Reference
# Check if Nginx is running
sudo systemctl status nginx
# Start Nginx
sudo systemctl start nginx
# Stop Nginx
sudo systemctl stop nginx
# Reload configuration (zero downtime)
sudo systemctl reload nginx
# Test configuration syntax
sudo nginx -t
# Enable a site
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Disable a site
sudo rm /etc/nginx/sites-enabled/example.com
# View access logs in real time
sudo tail -f /var/log/nginx/example.com.access.log
# View error logs in real time
sudo tail -f /var/log/nginx/example.com.error.log
What Comes After Nginx?
Once Nginx is installed and configured, the typical next steps in building a production server are:
- PHP and PHP-FPM — to run Laravel and other PHP applications
- Node.js — to run Express.js APIs and applications
- SSL certificates with Let's Encrypt — to serve everything over HTTPS
- MySQL or PostgreSQL — for your application databases
- Redis — for caching and queues
All of these work behind Nginx. The internet sees only Nginx. Everything else is internal.
Summary
Nginx is the backbone of modern web server infrastructure. Here is what you have learned in this guide:
- Nginx is a web server, reverse proxy, load balancer, and SSL terminator
- It uses an event-driven architecture that handles thousands of connections efficiently
- The reverse proxy pattern keeps your applications hidden from the internet
- Server blocks define how Nginx handles requests for each domain
- PHP applications use PHP-FPM, Node.js and Python apps use
proxy_pass - Always test configuration with
nginx -tbefore reloading - Use
reloadnotrestartin production to avoid downtime - Security headers and
server_tokens offare essential hardening steps
Nginx rewards the time you invest in learning it. Once you understand its architecture, managing multiple applications on a single server becomes straightforward, predictable, and reliable.
Top comments (0)