How to Install Nginx on Ubuntu 24.04 — Production Web Server & Reverse Proxy
Nginx runs a majority of the world's busiest websites for good reason: one process handles tens of thousands of concurrent connections on modest hardware, configuration is declarative and predictable, and the surface area for bugs is small. This guide walks you through installing Nginx on an Ubuntu 24.04 VPS from the official nginx.org repository — not the older Ubuntu-packaged version — then configuring it for production with HTTP/2, HTTP/3 QUIC, TLS, Brotli compression, rate limiting, and a tuned worker pool suitable for serving static sites and reverse-proxying application backends.
Skip the setup? Spin up a VPS with Nginx pre-configured as part of a LEMP or reverse-proxy stack. Launch a Starter VPS and have a hardened web server ready in under 60 seconds.
Table of Contents
Why Nginx for Production Web Serving?
Nginx earned its dominant position because of a single architectural choice: event-driven, asynchronous request handling. Where Apache traditionally spawned one process or thread per connection — hitting memory walls at a few thousand concurrent clients — Nginx uses a small number of worker processes, each running an epoll event loop that multiplexes thousands of connections at once. On a 2 vCPU / 4 GB VPS, Nginx can comfortably serve 10,000+ concurrent connections without breaking a sweat.
That efficiency translates into concrete production wins. TLS termination is CPU-bound, and Nginx's session cache and OCSP stapling implementations are among the fastest available. Static file serving uses the sendfile() syscall with zero-copy from kernel space, saturating the network interface before CPU becomes a bottleneck. Reverse proxying benefits from a mature upstream module that supports keepalive connections, health checks, weighted load balancing, and sticky sessions. HTTP/2 and HTTP/3 are first-class — the QUIC module shipped as production-ready in 2023.
Nginx also excels as the front-door process for multi-service deployments. You can terminate TLS, enforce rate limits, strip sensitive headers, gzip-compress responses, cache static assets, and route requests to three different backend pools — all with a few dozen lines of declarative configuration. Compared to programmatic gateways where the same logic lives in application code, Nginx keeps this concern at the infrastructure layer where it belongs.
If you are weighing alternatives, see our guides on Caddy (auto-HTTPS by default, simpler config), Traefik (container-native, excellent for Docker/Kubernetes), and Nginx Proxy Manager (a web UI on top of Nginx). For most traditional workloads — and for maximum flexibility with minimum overhead — raw Nginx remains the right choice.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server (PuTTY on Windows, or the built-in terminal on macOS/Linux)
- A domain name pointed at your server's IPv4 address (needed for TLS in Step 8)
- Ports 80, 443 (TCP) and 443 (UDP) open in any upstream firewall (UFW rules added below)
Recommended Plan: Starter>
For a single-server Nginx deployment serving a static site, a WordPress/PHP site, or reverse-proxying one or two backend apps, the Starter VPS plan is the right fit:>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth>
This handles thousands of concurrent connections and terminates TLS for multiple domains without strain. Scale up to Professional if you add heavy PHP-FPM workloads or plan to proxy Node.js apps under sustained load.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Refresh the package index and apply any pending security upgrades before adding a new repository.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Install a few utilities used later in this guide:
sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyringIf the kernel was updated, reboot once before continuing.
Step 2: Add the Official nginx.org Repository
Ubuntu ships its own nginx package, but it tracks an older version and is slow to receive security patches. The official nginx.org repository provides the latest stable branch signed by the Nginx maintainers. We will use it.
Import the signing key:
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/nullVerify the key fingerprint (it should end in 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62):
gpg --dry-run --quiet --no-keyring \
--import --import-options import-show \
/usr/share/keyrings/nginx-archive-keyring.gpgAdd the repository to apt. This configures the stable branch, which is the right default for production:
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.listIf you prefer the mainline branch (newer features, same quality bar per Nginx's own recommendation), replace packages/ with packages/mainline/:
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.listPin the repository so apt prefers it over any Ubuntu-packaged version:
sudo tee /etc/apt/preferences.d/99nginx > /dev/null <<EOF
Package: *
Pin: origin nginx.org
Pin: release o=nginx
Pin-Priority: 900
EOFRefresh the package index:
sudo apt updateStep 3: Install Nginx
With the repository in place, install Nginx:
sudo apt install -y nginxExpected output (abbreviated):
The following NEW packages will be installed:
nginx
...
Setting up nginx (1.27.x-1~noble) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /lib/systemd/system/nginx.service.Enable the service so it starts on boot and start it now:
sudo systemctl enable --now nginxOpen the firewall for HTTP, HTTPS (TCP) and QUIC (UDP):
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw reloadStep 4: Verify the Installation
Confirm the version that was installed:
nginx -vExpected output:
nginx version: nginx/1.27.4Check that the service is running:
sudo systemctl status nginxExpected output:
● nginx.service - nginx - high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-16 10:00:00 UTC; 10s ago
Main PID: 1234 (nginx)
Tasks: 3 (limit: 4567)
Memory: 4.0MFetch the default page to confirm Nginx is serving HTTP:
curl -I http://localhostExpected output:
HTTP/1.1 200 OK
Server: nginx/1.27.4
Content-Type: text/htmlVisit http://your-server-ip in a browser and you should see the "Welcome to nginx!" page.
Step 5: Understand the /etc/nginx Directory
The nginx.org package lays out configuration differently from the Ubuntu-packaged version. Here is the layout you get out of the box:
/etc/nginx/
├── conf.d/ # Per-site configs (included by nginx.conf)
│ └── default.conf # Default "welcome" server block
├── fastcgi_params # FastCGI parameter mappings (for PHP-FPM)
├── mime.types # File extension → MIME type mappings
├── nginx.conf # Main configuration entrypoint
├── scgi_params
├── uwsgi_paramsTwo important differences from Ubuntu's nginx package:
sites-available / sites-enabled structure by default. The nginx.org package uses conf.d/*.conf directly./var/log/nginx/access.log and /var/log/nginx/error.log.Many operators prefer the Debian/Ubuntu sites-available + sites-enabled convention because it lets you keep disabled sites on disk without Nginx loading them. We will recreate that pattern now.
Create the directories:
sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabledEdit /etc/nginx/nginx.conf and add the include directive inside the http { ... } block, just above the existing include /etc/nginx/conf.d/*.conf; line:
sudo nano /etc/nginx/nginx.confAdd:
include /etc/nginx/sites-enabled/*.conf;Disable the default welcome site so it does not collide with future configs:
sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.disabledTest the configuration and reload:
sudo nginx -t && sudo systemctl reload nginxThe nginx -t command validates syntax before applying changes. Make it a habit to run this before every reload — an invalid config never touches the running process this way.
Step 6: Configure a Static Site with Server Blocks
A server block in Nginx is equivalent to a virtual host in Apache — it defines how Nginx responds to a specific hostname. We will build one for a static site served from /var/www/example.com.
Create the document root and a placeholder index page:
sudo mkdir -p /var/www/example.com
sudo tee /var/www/example.com/index.html > /dev/null <<'EOF'
<!doctype html>
<html>
<head><title>example.com</title></head>
<body><h1>Served by Nginx on Ubuntu 24.04</h1></body>
</html>
EOF
sudo chown -R www-data:www-data /var/www/example.comWrite the server block to /etc/nginx/sites-available/example.com.conf:
sudo tee /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF' server { listen 80; listen [::]:80; server_name example.com www.example.com;root /var/www/example.com; index index.html;
access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log;
location / { try_files $uri $uri/ =404; }
# Deny access to hidden files like .git, .env location ~ /\. { deny all; access_log off; log_not_found off; } } EOF
Enable the site by symlinking into sites-enabled/:
sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/Test and reload:
sudo nginx -t && sudo systemctl reload nginxPoint your domain's A record at the server's IPv4 address, and http://example.com will serve your index page.
Step 7: Set Up a Reverse Proxy with Upstream
Most production deployments use Nginx as a reverse proxy in front of an application server — PHP-FPM, Node.js, Python (uWSGI/Gunicorn), Go, Ruby on Rails. For dynamic languages running HTTP servers (Node, Go, Python), the pattern is the same: define an upstream block and proxy_pass to it.
Assume you have a Node.js app listening on 127.0.0.1:3000. Create /etc/nginx/sites-available/app.example.com.conf:
sudo tee /etc/nginx/sites-available/app.example.com.conf > /dev/null <<'EOF' upstream app_backend { # Round-robin across multiple workers (add more lines for more workers) server 127.0.0.1:3000; # server 127.0.0.1:3001; # server 127.0.0.1:3002;# Keep connections open to the backend for reuse keepalive 32; }
server { listen 80; listen [::]:80; server_name app.example.com;
access_log /var/log/nginx/app.access.log; error_log /var/log/nginx/app.error.log;
location / { proxy_pass http://app_backend;
# Preserve client details for the app 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_set_header X-Forwarded-Host $host;
# Use HTTP/1.1 upstream so keepalive works proxy_http_version 1.1; proxy_set_header Connection "";
# WebSocket support proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
# Timeouts proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; } } EOF
The $connection_upgrade variable needs to be defined in the http { } block. Add this to /etc/nginx/nginx.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Enable and reload:
sudo ln -s /etc/nginx/sites-available/app.example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxLoad Balancing Strategies
The upstream block supports several algorithms:
- Round-robin (default) — Requests distributed evenly
least_conn;— Routes to the backend with the fewest active connectionsip_hash;— Same client IP always routes to the same backend (session stickiness)hash $request_uri consistent;— Consistent hashing by URL (useful for caching tiers)
fastcgi_pass block to use.Step 8: Obtain TLS Certificates with Certbot
No production site should run plain HTTP. Certbot from the Let's Encrypt project issues free, browser-trusted certificates and configures Nginx for you.
Install Certbot via snap (the upstream-recommended channel):
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbotRequest and install a certificate for both the apex and www hostnames:
sudo certbot --nginx -d example.com -d www.example.com \
--non-interactive --agree-tos --email [email protected] \
--redirectThe --redirect flag tells Certbot to add an HTTP-to-HTTPS redirect automatically. Certbot rewrites your server block to add the listen 443 ssl directive, the certificate paths, and a 301 redirect from port 80.
Verify the auto-renewal timer is armed:
sudo systemctl list-timers | grep certbotYou should see snap.certbot.renew.timer scheduled to run twice daily. Do a dry run to confirm renewals will succeed:
sudo certbot renew --dry-runStep 9: Enable HTTP/2 and HTTP/3 QUIC
Modern Nginx separates the listen directive from the protocol directive. Update the HTTPS server block in /etc/nginx/sites-available/example.com.conf:
server { # TCP: HTTP/1.1 and HTTP/2 listen 443 ssl; listen [::]:443 ssl; http2 on;# UDP: HTTP/3 over QUIC listen 443 quic reuseport; listen [::]:443 quic reuseport;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS only ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;
# Advertise HTTP/3 to clients on HTTP/2/1.1 add_header Alt-Svc 'h3=":443"; ma=86400' always;
root /var/www/example.com; index index.html;
location / { try_files $uri $uri/ =404; } }
A few notes:
reuseporton the QUIC listener lets multiple worker processes bind the same UDP port for better multi-core scaling.- Only one server block per
listen ... reuseportaddress may usereuseport— include it on the default/first server block listening on 443 QUIC. - The
Alt-Svcheader advertises HTTP/3 availability to browsers currently connected over HTTP/2, so their next request can upgrade.
sudo nginx -t && sudo systemctl reload nginx
curl -I --http3 https://example.comIf curl was built with HTTP/3 support, you will see HTTP/3 200. Otherwise test with a modern browser — Chrome, Firefox, and Safari all support HTTP/3.
Step 10: Add Rate Limiting
Rate limiting protects against brute-force login attempts, runaway bots, and accidental self-DoS from misbehaving clients. Nginx's limit_req_zone uses a token-bucket algorithm keyed by any variable — typically client IP.
Define a zone in the http { } block of /etc/nginx/nginx.conf:
# 10 MB of shared memory ≈ 160,000 unique IPs tracked
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;Then apply zones per-location in your server block:
server { # ... listen, TLS, server_name ...# Global rate limit: 30 req/s per IP, burst of 50 queued location / { limit_req zone=general burst=50 nodelay; try_files $uri $uri/ =404; }
# Strict rate limit for login: 5 req/min, burst of 3 delayed location /login { limit_req zone=login burst=3; proxy_pass http://app_backend; } }
Key parameters:
rate=30r/s— sustained request rate per keyburst=50— number of requests that may queue above the rate without being rejectednodelay— serve burst requests immediately instead of artificially delaying; requests aboveburststill get a503
503 Service Temporarily Unavailable by default. Customize with:limit_req_status 429;Set this in the http { } block so rate-limit rejections return 429 Too Many Requests, which is the semantically correct status code.
Step 11: Compile and Enable the Brotli Module
Brotli achieves 15-25% better compression than gzip on text. The nginx.org binary does not ship the Brotli module built in, but you can install it as a dynamic module.
Install the nginx-module-brotli package from the nginx.org repo:
sudo apt install -y nginx-module-brotliLoad the module at the top of /etc/nginx/nginx.conf — before the events { } block:
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;Enable compression inside the http { } block (keep gzip as a fallback):
# Gzip fallback for older clients
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
image/svg+xml;Brotli for modern clients
brotli on;
brotli_comp_level 5;
brotli_static on;
brotli_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
image/svg+xml;Tips:
brotli_comp_level 5is the sweet spot — higher levels spend more CPU for marginal gains.brotli_static onserves pre-compressed.brfiles if they exist next to the original, avoiding runtime CPU cost entirely.- Nginx negotiates per-request: Brotli-capable clients get
.br, older clients get gzip, and ancient clients get uncompressed bytes.
sudo nginx -t && sudo systemctl reload nginx
curl -H "Accept-Encoding: br" -I https://example.com/style.cssThe response should include Content-Encoding: br.
Step 12: Security Headers
Browsers respect a handful of HTTP headers that materially reduce XSS, clickjacking, and MIME-confusion attacks. Add them inside the server { } block (or http { } for site-wide defaults):
# Force HTTPS for 1 year including subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;Deny framing (clickjacking protection)
add_header X-Frame-Options "DENY" always;Restrict referrer leakage
add_header Referrer-Policy "strict-origin-when-cross-origin" always;Restrict what the browser is allowed to load
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self';" always;Disable dangerous browser features
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;The always flag ensures the headers are sent even on error responses (4xx, 5xx), which is important for HSTS.
Validate after reload at securityheaders.com — aim for an A or A+ grade.
Step 13: Custom Log Format and Caching Headers
The default combined log format is fine, but for performance analysis you want request duration and upstream timing. Define a richer format in /etc/nginx/nginx.conf:
log_format detailed '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'rt=$request_time uct="$upstream_connect_time" ' 'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log detailed;
Now each log line captures:
rt— total request time from first byte in to last byte outuct— time spent connecting to the upstreamuht— time until upstream sent response headersurt— total upstream response time
goaccess or ship it to your log aggregation stack for p95/p99 dashboards.Caching Headers for Static Assets
Versioned static assets (e.g. /static/app.a1b2c3.js) can be cached aggressively. Add a location block:
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|woff2?|ttf|eot|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}For HTML pages you generally want short TTLs and revalidation:
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
expires 0;
}immutable tells browsers never to revalidate versioned files — perfect for content-hashed filenames emitted by Webpack, Vite, esbuild, etc.
Step 14: Worker and Connection Tuning
Nginx's defaults are conservative. For a production VPS you can safely tune a few values at the top of /etc/nginx/nginx.conf.
# Use one worker per CPU core worker_processes auto;Bind workers to specific cores (reduces cache thrashing on 4+ core systems)
worker_cpu_affinity auto;File descriptors per worker — must be higher than worker_connections
worker_rlimit_nofile 65535;events { # Max simultaneous connections per worker worker_connections 4096;
# Accept as many connections as possible per event loop pass multi_accept on;
# Linux: epoll is already the default, but explicit is good use epoll; }
http { # Cache open file descriptors for static assets open_file_cache max=10000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on;
# Faster static file serving sendfile on; tcp_nopush on; tcp_nodelay on;
# Keepalive keepalive_timeout 65s; keepalive_requests 1000;
# Hide Nginx version from responses and error pages server_tokens off;
# Generous hash table sizes for many server blocks types_hash_max_size 2048; server_names_hash_bucket_size 128;
# Client buffer sizes (tune if you accept large uploads) client_max_body_size 10m; client_body_buffer_size 128k; client_header_buffer_size 4k; large_client_header_buffers 4 16k; }
Also raise the systemd unit's file-descriptor limit so worker_rlimit_nofile actually takes effect:
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo tee /etc/systemd/system/nginx.service.d/override.conf > /dev/null <<EOF
[Service]
LimitNOFILE=100000
EOF
sudo systemctl daemon-reload
sudo systemctl restart nginxVerify the running worker's effective limits:
cat /proc/$(pgrep -f "nginx: worker" | head -1)/limits | grep "open files"Expected output:
Max open files 100000 100000 filesRough Capacity Math
With the tuning above, a 2 vCPU / 4 GB Starter VPS can handle:
- 2 workers × 4096 connections = 8,192 concurrent connections
- For static content with
sendfile, this saturates a 1 Gbps uplink long before CPU peaks. - For reverse-proxied dynamic content, backend capacity becomes the bottleneck — not Nginx.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) | Another web server (Apache, Caddy) is bound to port 80 | Stop the conflicting service: sudo systemctl stop apache2 and sudo systemctl disable apache2 |
nginx: [emerg] "server" directive is not allowed here | A server { } block placed outside the http { } block | Ensure all server blocks live inside files included by http { } — typically sites-enabled/*.conf |
502 Bad Gateway when proxying | Upstream app not running or bound to wrong interface | Check the upstream with curl http://127.0.0.1:3000. Verify the proxy_pass URL matches exactly. |
413 Request Entity Too Large on upload | client_max_body_size too small | Raise the limit in the relevant server or location block, e.g. client_max_body_size 50m; |
| HTTP/3 not negotiated in browser | UDP 443 blocked, or listen ... quic reuseport duplicated | Confirm UFW allows UDP 443. Ensure only one server block uses reuseport on the same address. |
Certbot renewal fails with No such file or directory | Nginx config changed and broke nginx -t | Run sudo nginx -t to locate the syntax error, then sudo certbot renew |
High urt but low uct in logs | Upstream app is slow; Nginx is fine | Profile the backend application, not Nginx. Add keepalive to the upstream block to skip reconnect overhead. |
Reading the Error Log
The error log is where Nginx tells you exactly what is wrong:
sudo tail -f /var/log/nginx/error.logSet the log level in nginx.conf during debugging:
error_log /var/log/nginx/error.log debug;Revert to warn or error once the issue is resolved — debug logs are enormous.
FAQ
Should I install Nginx from the Ubuntu repo or from nginx.org?
For production use, install from the official nginx.org repository. The Ubuntu-packaged version lags several releases behind and often misses security patches and new features like HTTP/3 QUIC support. The nginx.org repo provides the latest mainline and stable builds signed by the Nginx team, with faster patch turnaround and the same binary Nginx ships to its own customers.
What is the difference between the mainline and stable Nginx branches?
Stable receives only critical bug fixes and security patches — it is the conservative choice for long-running production servers. Mainline receives all new features, performance improvements, and bug fixes on a faster release cadence. For most production workloads mainline is actually recommended by Nginx itself because it gets fixes first. Pick stable only if you have strict change-control requirements.
Do I need HTTP/3 if I already have HTTP/2 enabled?
HTTP/3 over QUIC delivers noticeably lower latency on lossy mobile networks because it eliminates head-of-line blocking at the transport layer and has a 0-RTT handshake for returning visitors. For desktop users on fast fibre connections the improvement is marginal. If your audience is mobile-heavy or geographically distant from your server, enable HTTP/3 — otherwise HTTP/2 alone is fine.
How many worker processes should I configure on a 4-core VPS?
Leave worker_processes set to auto — Nginx will detect the CPU count and spawn one worker per core, which is the recommended configuration for the vast majority of workloads. Each worker handles thousands of concurrent connections via epoll, so you do not need more workers than cores. Tune worker_connections (default 1024) upward if you expect more than 4000 concurrent connections on a 4-core box.
Can Nginx replace a full application server like PHP-FPM or Node.js?
No. Nginx serves static files directly and proxies dynamic requests to an application server. For PHP sites, Nginx passes requests to PHP-FPM over a FastCGI socket. For Node.js, Python, Ruby, or Go applications, Nginx reverse-proxies over HTTP to the app listening on a local port. This separation is intentional — Nginx handles TLS, static assets, and connection management while your app focuses on business logic.
How do I renew my Let's Encrypt certificates automatically?
Certbot installs a systemd timer (certbot.timer) that runs twice daily and renews any certificate within 30 days of expiry. Verify with systemctl list-timers | grep certbot. Nginx is reloaded automatically via the --deploy-hook when a renewal succeeds. You can force a dry-run renewal with sudo certbot renew --dry-run to confirm the pipeline works before expiry approaches.
Is Brotli worth enabling over gzip?
Yes for text assets (HTML, CSS, JS, SVG, JSON). Brotli achieves 15-25% better compression ratios than gzip at equivalent CPU cost, which reduces bandwidth and improves Largest Contentful Paint scores. All modern browsers support it. Keep gzip enabled as a fallback for older clients — Nginx will automatically serve whichever the client accepts.
Next Steps
With Nginx hardened and humming, here is where to go next:
- Switch to an easier TLS story with Caddy — If you found Certbot setup cumbersome, Caddy issues and renews certificates automatically with zero config. It trades some flexibility for a dramatically simpler experience.
- Run Nginx behind Traefik for Docker workloads — If you are deploying containerized services, Traefik auto-discovers containers via Docker labels and handles routing and TLS. Combine it with Nginx as a backend for mature web-serving needs.
- Manage Nginx through a web UI — Nginx Proxy Manager wraps raw Nginx in a friendly browser UI with Let's Encrypt built in — handy if you want colleagues who do not know Nginx syntax to add new proxies.
- Add PHP-FPM for WordPress and Laravel — Our PHP-FPM on Ubuntu guide walks through the FastCGI socket setup and the exact
fastcgi_passblock to drop into the server blocks you created here.
- Dig deeper into Nginx internals — The official documentation at nginx.org/en/docs/ covers every directive, module, and tuning knob in exhaustive detail. The "Beginner's Guide" and "Admin's Guide" sections are particularly good follow-up reading.
Skip the Manual Install — Get a Production-Ready VPS>
Our Starter VPS plans give you a clean Ubuntu 24.04 environment with full root access and unmetered bandwidth — perfect for hosting Nginx in front of your sites and applications.>
- 2 vCPU, 4 GB RAM, 50 GB NVMe SSD
- Root SSH access on first boot
- UFW and fail2ban pre-installed on request
- IPv4 and IPv6 networking included>
Deploy Your Starter VPS Now — hardened Nginx in under 30 minutes.