/

August 27, 2025

Ubuntu 24.04: Production‑grade NGINX with HTTP/3 & Let’s Encrypt (step‑by‑step)

CONTENTS

Picture of alexbolt
alexbolt
Senior DevOps, Head of DevOps, Lead Developer
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
Admins and developers deploying production web stacks on Ubuntu 24.04. You should be comfortable with a shell and have sudo access.
Prerequisites
•A domain (e.g., example.com) with A/AAAA DNS pointing to this server.
•A sudo user on Ubuntu 24.04 LTS.
•Port 80/443 reachable from the internet.
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'
<!doctype html>
<meta charset="utf-8">
<title>example.com</title>
<h1>It works </h1>
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'
<!doctype html>
<meta charset="utf-8">
<title>example.com</title>
<h1>It works </h1>
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 ( apt packages above) and that your NGINX loads dynamic modules by default on Ubuntu.

curl --http3 fails

  • Your build may not have HTTP/3; confirm nginx -V shows http_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.

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments