Nginx is a lightweight, event-driven web server that is also widely used as a reverse proxy and load balancer. It serves static files very efficiently and keeps memory usage flat under many concurrent connections. In this tutorial you will install Nginx on Ubuntu 24.04, allow it through the firewall, serve your own site from a server block, harden the default configuration, add basic rate limiting and secure the site with a Let's Encrypt certificate. Differences for Rocky Linux 9 are summarized at the end.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - A domain name with an
Arecord pointing to your server's public IP address. This guide usesyour_domainas a placeholder. The domain is only required for the HTTPS step. - Ports 80 and 443 reachable from the internet, and nothing else (such as Apache) listening on them.
Step 1 - Installing Nginx
Nginx is available in Ubuntu's default repositories. Refresh the package index and install it:
sudo apt update
sudo apt install nginx
The service starts automatically and is enabled at boot. Check it:
sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-24 10:20:41 UTC; 9s ago
Press q to exit. Confirm the version:
nginx -v
nginx version: nginx/1.24.0 (Ubuntu)
NoteUbuntu's package receives security fixes for the life of the release, which is what most servers need. If you require features from a newer release, nginx.org publishes its own repository for Ubuntu; follow the instructions at nginx.org/en/linux_packages.html. Its packages use
/etc/nginx/conf.d/instead ofsites-available, so adapt the paths in this guide.
Step 2 - Allowing Nginx through the firewall
Nginx registers three UFW application profiles:
sudo ufw app list
Available applications:
Nginx Full
Nginx HTTP
Nginx HTTPS
OpenSSH
Allow both HTTP and HTTPS with Nginx Full:
sudo ufw allow 'Nginx Full'
If UFW is not enabled yet, allow SSH first so you keep access, then enable the firewall:
sudo ufw allow OpenSSH
sudo ufw enable
Verify:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Nginx Full (v6) ALLOW Anywhere (v6)
Request the default page:
curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html
Content-Length: 615
Opening http://your_server_ip in a browser shows the "Welcome to nginx!" page.
Step 3 - Understanding the configuration layout
On Ubuntu, Nginx's configuration lives in /etc/nginx/:
| Path | Purpose |
|---|---|
/etc/nginx/nginx.conf | Main file: global settings and the http block. |
/etc/nginx/sites-available/ | One file per site (server block). |
/etc/nginx/sites-enabled/ | Symbolic links to the sites that are active. |
/etc/nginx/conf.d/ | Extra *.conf files included inside the http block. |
/etc/nginx/snippets/ | Reusable fragments you pull in with include. |
/var/www/html/ | Document root of the default site. |
/var/log/nginx/ | access.log and error.log. |
Configuration is organized in nested contexts. The http block holds settings shared by all sites, each server block defines one site, and location blocks inside a server decide how specific URLs are handled. Directives set in an outer context are inherited by inner ones unless overridden.
Always test before applying a change, then reload. A reload applies the new configuration without dropping connections, and if the test fails the running configuration stays untouched:
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Step 4 - Creating a server block for your site
Create a document root for your domain and give ownership to your user so you can deploy files without sudo:
sudo mkdir -p /var/www/your_domain/html
sudo chown -R "$USER":"$USER" /var/www/your_domain/html
Add a test page:
nano /var/www/your_domain/html/index.html
<!DOCTYPE html>
<html>
<head><title>your_domain</title></head>
<body><h1>It works: your_domain is served by Nginx</h1></body>
</html>
Create the server block:
sudo nano /etc/nginx/sites-available/your_domain
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
root /var/www/your_domain/html;
index index.html;
access_log /var/log/nginx/your_domain.access.log;
error_log /var/log/nginx/your_domain.error.log;
location / {
try_files $uri $uri/ =404;
}
location ~ /\. {
deny all;
}
}
try_files serves the requested file or directory and returns 404 otherwise, and the second location blocks access to hidden files such as .git or .env.
Enable the site by linking it into sites-enabled, remove the default site, then test and reload:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Removing the link only disables the default site; the original stays in sites-available. Test the new site with a Host header, which works even before DNS propagates:
curl -H "Host: your_domain" http://localhost
<!DOCTYPE html>
<html>
<head><title>your_domain</title></head>
<body><h1>It works: your_domain is served by Nginx</h1></body>
</html>
For more patterns (multiple domains, a catch-all default server, reverse proxying to an application), see server blocks in Nginx.
Step 5 - Hardening the defaults
Nginx shows its version in the Server header and on error pages. Open the main configuration file:
sudo nano /etc/nginx/nginx.conf
Inside the http { ... } block, find the commented line # server_tokens off; and uncomment it:
server_tokens off;
Next, add security headers for every site. Create a snippet:
sudo nano /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Include it in your server block, directly under the index line:
sudo nano /etc/nginx/sites-available/your_domain
include snippets/security-headers.conf;
Important
add_headerdirectives are inherited only when the inner block defines none of its own. If you add anadd_headerinside alocation, include the snippet there too, or those headers disappear for that location.
Test, reload and check the headers:
sudo nginx -t
sudo systemctl reload nginx
curl -I -H "Host: your_domain" http://localhost
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Step 6 - Tuning compression and adding rate limiting
Ubuntu's nginx.conf already enables gzip on;, but by default only HTML is compressed. In the same http block, uncomment the gzip_types line so CSS, JavaScript, JSON and SVG are compressed too:
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
Rate limiting protects login forms and APIs from brute force and simple floods. Define a shared zone in a file under conf.d/, which Nginx includes inside the http block:
sudo nano /etc/nginx/conf.d/rate-limit.conf
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_status 429;
This tracks clients by IP address in 10 MB of shared memory (roughly 160,000 addresses) and allows 10 requests per second each. Apply it in your server block's location /:
location / {
limit_req zone=perip burst=20 nodelay;
try_files $uri $uri/ =404;
}
burst=20 nodelay lets a browser load a page with many assets at once while still rejecting sustained excess traffic with HTTP 429. Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Verify compression:
curl -s -o /dev/null -D - -H "Host: your_domain" -H "Accept-Encoding: gzip" http://localhost/ | grep -i content-encoding
Content-Encoding: gzip
The defaults worker_processes auto; (one worker per CPU core) and worker_connections 768; are sensible for most servers. Only raise worker_connections if the error log reports worker_connections are not enough.
Step 7 - Enabling HTTPS with Let's Encrypt
Once your_domain resolves to the server, install Certbot and its Nginx plugin:
sudo apt install certbot python3-certbot-nginx
Request a certificate. Certbot finds the matching server_name and edits the server block for you:
sudo certbot --nginx -d your_domain -d www.your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at: /etc/letsencrypt/live/your_domain/privkey.pem
...
Successfully deployed certificate for your_domain to /etc/nginx/sites-enabled/your_domain
Congratulations! You have successfully enabled HTTPS on https://your_domain and https://www.your_domain
Certbot adds listen 443 ssl lines, the certificate paths, a recommended TLS configuration and a redirect from HTTP to HTTPS. A systemd timer renews the certificate automatically; test it with:
sudo certbot renew --dry-run
Confirm HTTPS from the command line:
curl -I https://your_domain
HTTP/1.1 200 OK
Server: nginx
Step 8 - Reading logs
Follow your site's logs while you browse to it:
sudo tail -f /var/log/nginx/your_domain.access.log /var/log/nginx/your_domain.error.log
Problems with the service itself appear in the journal:
sudo journalctl -u nginx --since "1 hour ago"
Logs are rotated daily and kept for 14 days by /etc/logrotate.d/nginx.
Rocky Linux 9 differences
| Item | Ubuntu 24.04 | Rocky Linux 9 |
|---|---|---|
| Install | apt install nginx | sudo dnf install nginx, then sudo systemctl enable --now nginx |
| Site files | sites-available/ + symlink | *.conf files in /etc/nginx/conf.d/ |
| Default document root | /var/www/html | /usr/share/nginx/html |
| Worker user | www-data | nginx |
| Firewall | ufw allow 'Nginx Full' | firewall-cmd --permanent --add-service=http --add-service=https and firewall-cmd --reload |
| Certbot | apt install python3-certbot-nginx | Enable EPEL (sudo dnf install epel-release), then sudo dnf install certbot python3-certbot-nginx |
SELinux is enforcing on Rocky Linux. Content under /var/www/ gets the right label after sudo restorecon -Rv /var/www/your_domain, and if Nginx proxies to a backend application you must allow it with sudo setsebool -P httpd_can_network_connect 1.
Troubleshooting
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use). Another service holds port 80. Identify it with sudo ss -tlnp | grep ':80 '; it is often Apache (sudo systemctl disable --now apache2).
could not build server_names_hash, you should increase server_names_hash_bucket_size. This appears with long or many domain names. Uncomment server_names_hash_bucket_size 64; in the http block of nginx.conf.
403 Forbidden. Either there is no index.html in the root or the www-data user cannot read the files. Check the permissions along the path with namei -l /var/www/your_domain/html/index.html.
413 Request Entity Too Large. The default upload limit is 1 MB. Raise it in the server block, for example client_max_body_size 50M;, then reload.
502 Bad Gateway. Nginx cannot reach the backend it proxies to (PHP-FPM, Node.js and so on). The site's error log names the upstream address; confirm the backend is running and listening there.
Conclusion
Nginx is now running on Ubuntu 24.04 with a dedicated server block, version information hidden, security headers, compression for text assets, per-IP rate limiting and an auto-renewing HTTPS certificate. From here you can host additional sites with Nginx server blocks, put Nginx in front of an application as a reverse proxy, or compare the trade-offs in the Apache vs Nginx comparison.
