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 sudo privileges.
  • A domain name with an A record pointing to your server's public IP address. This guide uses your_domain as 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)

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/:

PathPurpose
/etc/nginx/nginx.confMain 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;

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

ItemUbuntu 24.04Rocky Linux 9
Installapt install nginxsudo dnf install nginx, then sudo systemctl enable --now nginx
Site filessites-available/ + symlink*.conf files in /etc/nginx/conf.d/
Default document root/var/www/html/usr/share/nginx/html
Worker userwww-datanginx
Firewallufw allow 'Nginx Full'firewall-cmd --permanent --add-service=http --add-service=https and firewall-cmd --reload
Certbotapt install python3-certbot-nginxEnable 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.