Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Nginx Ubuntu
GUIDEInstall Guides

How to Install Nginx on Ubuntu 24.04 — Production Web Server & Reverse Proxy

23 min read

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?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Add the Official nginx.org Repository
  • Step 3: Install Nginx
  • Step 4: Verify the Installation
  • Step 5: Understand the /etc/nginx Directory
  • Step 6: Configure a Static Site with Server Blocks
  • Step 7: Set Up a Reverse Proxy with Upstream
  • Step 8: Obtain TLS Certificates with Certbot
  • Step 9: Enable HTTP/2 and HTTP/3 QUIC
  • Step 10: Add Rate Limiting
  • Step 11: Compile and Enable the Brotli Module
  • Step 12: Security Headers
  • Step 13: Custom Log Format and Caching Headers
  • Step 14: Worker and Connection Tuning
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Refresh the package index and apply any pending security upgrades before adding a new repository.

    bash
    sudo apt update && sudo apt upgrade -y

    Expected output (abbreviated):

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

    bash
    sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring

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

    bash
    curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
      | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/null

    Verify the key fingerprint (it should end in 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62):

    bash
    gpg --dry-run --quiet --no-keyring \
      --import --import-options import-show \
      /usr/share/keyrings/nginx-archive-keyring.gpg

    Add the repository to apt. This configures the stable branch, which is the right default for production:

    bash
    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.list

    If you prefer the mainline branch (newer features, same quality bar per Nginx's own recommendation), replace packages/ with packages/mainline/:

    bash
    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.list

    Pin the repository so apt prefers it over any Ubuntu-packaged version:

    bash
    sudo tee /etc/apt/preferences.d/99nginx > /dev/null <<EOF
    Package: *
    Pin: origin nginx.org
    Pin: release o=nginx
    Pin-Priority: 900
    EOF

    Refresh the package index:

    bash
    sudo apt update

    Step 3: Install Nginx

    With the repository in place, install Nginx:

    bash
    sudo apt install -y nginx

    Expected output (abbreviated):

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

    bash
    sudo systemctl enable --now nginx

    Open the firewall for HTTP, HTTPS (TCP) and QUIC (UDP):

    bash
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 443/udp
    sudo ufw reload

    Step 4: Verify the Installation

    Confirm the version that was installed:

    bash
    nginx -v

    Expected output:

    text
    nginx version: nginx/1.27.4

    Check that the service is running:

    bash
    sudo systemctl status nginx

    Expected output:

    text
    ● 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.0M

    Fetch the default page to confirm Nginx is serving HTTP:

    bash
    curl -I http://localhost

    Expected output:

    text
    HTTP/1.1 200 OK
    Server: nginx/1.27.4
    Content-Type: text/html

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

    text
    /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_params

    Two important differences from Ubuntu's nginx package:

  • There is no sites-available / sites-enabled structure by default. The nginx.org package uses conf.d/*.conf directly.
  • Logs live in /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:

    bash
    sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled

    Edit /etc/nginx/nginx.conf and add the include directive inside the http { ... } block, just above the existing include /etc/nginx/conf.d/*.conf; line:

    bash
    sudo nano /etc/nginx/nginx.conf

    Add:

    nginx
    include /etc/nginx/sites-enabled/*.conf;

    Disable the default welcome site so it does not collide with future configs:

    bash
    sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.disabled

    Test the configuration and reload:

    bash
    sudo nginx -t && sudo systemctl reload nginx

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

    bash
    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.com

    Write the server block to /etc/nginx/sites-available/example.com.conf:

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

    bash
    sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/

    Test and reload:

    bash
    sudo nginx -t && sudo systemctl reload nginx

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

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

    nginx
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    Enable and reload:

    bash
    sudo ln -s /etc/nginx/sites-available/app.example.com.conf /etc/nginx/sites-enabled/
    sudo nginx -t && sudo systemctl reload nginx

    Load Balancing Strategies

    The upstream block supports several algorithms:

    • Round-robin (default) — Requests distributed evenly
    • least_conn; — Routes to the backend with the fewest active connections
    • ip_hash; — Same client IP always routes to the same backend (session stickiness)
    • hash $request_uri consistent; — Consistent hashing by URL (useful for caching tiers)
    For a PHP site, proxy requests to a local PHP-FPM socket instead. See our PHP-FPM on Ubuntu guide for the exact 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):

    bash
    sudo snap install core
    sudo snap refresh core
    sudo snap install --classic certbot
    sudo ln -s /snap/bin/certbot /usr/bin/certbot

    Request and install a certificate for both the apex and www hostnames:

    bash
    sudo certbot --nginx -d example.com -d www.example.com \
      --non-interactive --agree-tos --email [email protected] \
      --redirect

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

    bash
    sudo systemctl list-timers | grep certbot

    You should see snap.certbot.renew.timer scheduled to run twice daily. Do a dry run to confirm renewals will succeed:

    bash
    sudo certbot renew --dry-run

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

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

    • reuseport on the QUIC listener lets multiple worker processes bind the same UDP port for better multi-core scaling.
    • Only one server block per listen ... reuseport address may use reuseport — include it on the default/first server block listening on 443 QUIC.
    • The Alt-Svc header advertises HTTP/3 availability to browsers currently connected over HTTP/2, so their next request can upgrade.
    Reload and test:

    bash
    sudo nginx -t && sudo systemctl reload nginx
    curl -I --http3 https://example.com

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

    nginx
    # 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:

    nginx
    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 key
    • burst=50 — number of requests that may queue above the rate without being rejected
    • nodelay — serve burst requests immediately instead of artificially delaying; requests above burst still get a 503
    Clients that exceed the limit receive HTTP 503 Service Temporarily Unavailable by default. Customize with:

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

    bash
    sudo apt install -y nginx-module-brotli

    Load the module at the top of /etc/nginx/nginx.conf — before the events { } block:

    nginx
    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):

    nginx
    # 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 5 is the sweet spot — higher levels spend more CPU for marginal gains.
    • brotli_static on serves pre-compressed .br files 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.
    Reload and verify:

    bash
    sudo nginx -t && sudo systemctl reload nginx
    curl -H "Accept-Encoding: br" -I https://example.com/style.css

    The 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):

    nginx
    # 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:

    nginx
    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 out
    • uct — time spent connecting to the upstream
    • uht — time until upstream sent response headers
    • urt — total upstream response time
    Tail this log with 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:

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

    nginx
    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.

    nginx
    # 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:

    bash
    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 nginx

    Verify the running worker's effective limits:

    bash
    cat /proc/$(pgrep -f "nginx: worker" | head -1)/limits | grep "open files"

    Expected output:

    text
    Max open files            100000               100000               files

    Rough 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.
    Scale up to 4 or 6 vCPU plans when you start proxying heavy PHP-FPM or WebSocket workloads, not because Nginx itself is struggling.

    Troubleshooting

    ProblemCauseSolution
    nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)Another web server (Apache, Caddy) is bound to port 80Stop the conflicting service: sudo systemctl stop apache2 and sudo systemctl disable apache2
    nginx: [emerg] "server" directive is not allowed hereA server { } block placed outside the http { } blockEnsure all server blocks live inside files included by http { } — typically sites-enabled/*.conf
    502 Bad Gateway when proxyingUpstream app not running or bound to wrong interfaceCheck the upstream with curl http://127.0.0.1:3000. Verify the proxy_pass URL matches exactly.
    413 Request Entity Too Large on uploadclient_max_body_size too smallRaise the limit in the relevant server or location block, e.g. client_max_body_size 50m;
    HTTP/3 not negotiated in browserUDP 443 blocked, or listen ... quic reuseport duplicatedConfirm UFW allows UDP 443. Ensure only one server block uses reuseport on the same address.
    Certbot renewal fails with No such file or directoryNginx config changed and broke nginx -tRun sudo nginx -t to locate the syntax error, then sudo certbot renew
    High urt but low uct in logsUpstream app is slow; Nginx is fineProfile 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:

    bash
    sudo tail -f /var/log/nginx/error.log

    Set the log level in nginx.conf during debugging:

    nginx
    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_pass block 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket