CONTENTS
Title
Ubuntu 24.04: Production‑grade NGINX with HTTP/3 & Let’s Encrypt (step‑by‑step)
Last updated: 27 Aug 2025 • Tested on: Ubuntu 24.04 LTS • Estimated time: 25–40 minutes
Outcome: a secure, fast NGINX with HTTP/2 (and optional HTTP/3/QUIC), automatic TLS renewal, sane defaults, and basic bot/rate limiting — ready for static sites or as a reverse proxy for apps (PHP, Node, Python).
Who this is for
Quick start (TL;DR)
# 0) Update
sudo apt update && sudo apt -y upgrade
# 1) Install NGINX + firewall + Certbot (snap)
sudo apt -y install nginx ufw snapd
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# 2) Open firewall
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full" # 80/tcp, 443/tcp
sudo ufw --force enable
# 3) Create a site
sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'CONF'
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; }
}
CONF
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 4) Issue and install a Let’s Encrypt cert
sudo certbot --nginx -d example.com -d www.example.com --redirect \
--agree-tos -m admin@example.com --no-eff-email
# 5) Verify auto‑renewal
sudo systemctl status snap.certbot.renew.timer --no-pager
sudo certbot renew --dry-run
Step 1 — Install NGINX, UFW, and Certbot
Verify:
nginx -v
systemctl is-active nginx
Enable the firewall and open web ports:
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw --force enable
sudo ufw status
Step 2 — Create a minimal site
sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
sudo tee /var/www/example.com/public/index.html >/dev/null <<'HTML'
example.com
It works
HTML
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'CONF'
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; }
}
CONF
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Verify:
curl -I http://example.com
should return
HTTP/1.1 200 OK
.
Step 3 — Add HTTPS with Let’s Encrypt
Issue the certificate and let Certbot edit the NGINX config automatically:
sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
sudo tee /var/www/example.com/public/index.html >/dev/null <<'HTML'
example.com
It works
HTML
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'CONF'
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; }
}
CONF
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Certbot creates a 443 server block with ssl_certificate and ssl_certificate_key, and redirects http -> https.
Verify:
curl -I https://example.com | grep -i "strict-transport-security\|server\|^HTTP"
Step 4 — Enable HTTP/2 (always) and HTTP/3 (optional)
HTTP/2 is on when your 443 listener includes
http2
:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
...
}
Optional: HTTP/3/QUIC
Check if your NGINX build supports HTTP/3:
nginx -V 2>&1 | grep -o http_v3_module || echo "no http/3 module"
If present, add QUIC listeners to your TLS server block:
# Add alongside the existing http2 listeners
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
# Advertise HTTP/3 (QUIC)
add_header Alt-Svc 'h3=":443"; ma=86400' always;
If you don’t see
http_v3_module
, you can:
stay on HTTP/2 (totally fine), or
install an NGINX build with HTTP/3 support (e.g., nginx.org mainline). Follow the vendor’s instructions and repeat
nginx -V.
Verify:
# HTTP/2
curl -I --http2 https://example.com | head -n1
# HTTP/3 (if enabled)
curl -I --http3 https://example.com | head -n1
Step 5 — TLS & security hardening
Create a hardening include (applies to all TLS vhosts):
sudo tee /etc/nginx/conf.d/ssl_security.conf >/dev/null <<'CONF'
# Reasonable TLS defaults
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# OCSP stapling (requires valid resolver)
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 9.9.9.9 valid=300s;
resolver_timeout 5s;
# Security headers (adjust for your app)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
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;
# Hide version
server_tokens off;
CONF
sudo nginx -t && sudo systemctl reload nginx
Step 6 — Basic rate limiting & bad bot screening
sudo tee /etc/nginx/conf.d/security_limits.conf >/dev/null <<'CONF'
# Simple global rate limit (tune for your traffic)
limit_req_zone $binary_remote_addr zone=req:10m rate=10r/s;
# Very naive bot flag (adjust regex list to your risk profile)
map $http_user_agent $is_badbot {
default 0;
~*(crawler|scanner|dirbuster|sqlmap|nikto) 1;
}
CONF
Attach to your server block(s):
server {
# ...
if ($is_badbot) { return 403; }
location / {
limit_req zone=req burst=20 nodelay;
try_files $uri $uri/ =404;
}
}
Reload NGINX service after edits.
Step 7 — Brotli compression (optional but recommended)
Install dynamic modules (Ubuntu packages):
sudo apt -y install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
Enable in an include:
sudo tee /etc/nginx/conf.d/brotli.conf >/dev/null <<'CONF'
# Enable Brotli if module is present
brotli on;
brotli_comp_level 5;
brotli_static on;
# Common MIME types (add others as needed)
brotli_types text/plain text/css text/xml application/xml application/json \
application/javascript application/x-javascript application/manifest+json \
application/rss+xml application/atom+xml image/svg+xml font/ttf font/otf;
CONF
sudo nginx -t && sudo systemctl reload nginx
Note: Keep Gzip enabled as a fallback for older clients if you wish.
Step 8 — Logging & ops hygiene
Set
error_log /var/log/nginx/error.log warn;Use logrotate (default on Ubuntu) and monitor disk.
Put
/etc/nginx/under git and deploy via CI.Add uptime and TLS‑expiry monitors (Uptime-Kuma, Healthchecks, etc.).
Step 9 — Health checks
# HTTP → HTTPS redirect
curl -I http://example.com | grep -i location
# TLS works
curl -I https://example.com | head -n1
# HTTP/2 & HTTP/3 (if enabled)
curl -I --http2 https://example.com | head -n1
curl -I --http3 https://example.com | head -n1
# Certbot timer present
systemctl list-timers | grep -i certbot
Reverse proxy snippet (optional)
Use this if your app runs on an upstream (e.g., PHP-FPM at 127.0.0.1:9000 or Node on :3000).
location / {
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_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Troubleshooting
Certbot says DNS problem / timeout
Wait for DNS to propagate; ensure ports 80/443 are open; disable any CDN orange‑proxy during issuance.
nginx: [emerg] unknown directive 'brotli'
Ensure the Brotli modules are installed/enabled (
aptpackages above) and that your NGINX loads dynamic modules by default on Ubuntu.
curl --http3
fails
Your build may not have HTTP/3; confirm
nginx -Vshowshttp_v3_module. HTTP/2 is fine for production.
413 Request Entity Too Large
Increase body size in the relevant server/location:
client_max_body_size 50m;
SSL Labs grade not A+
Confirm HSTS header, modern ciphers, and OCSP stapling. Re‑test after reloads.
FAQ
Is HTTP/3 required? No. HTTP/2 delivers excellent real‑world performance. Treat HTTP/3 as an incremental upgrade when your stack supports it.
Can I copy this to Debian 12 or Rocky/Alma 9? Yes, with minor package manager differences. Certbot via snap also works broadly.
Do I need a CDN? Not strictly. For dynamic apps, focus on proper caching (NGINX proxy_cache or Varnish) and origin performance first. A CDN is a later optimization.

