Nginx Complete Guide and Cheatsheet

Learn Nginx from the ground up with this complete guide and cheatsheet. From installing the latest version to configuration, security, and performance optimization.

 •  Updated on Jan 14, 2026  •  30 min read  •  ••• views
Featured image for Nginx Complete Guide and Cheatsheet

Introduction

Nginx (engine x) is a powerful, high-performance web server and reverse proxy server that has gained immense popularity due to its speed, scalability, and flexibility. It is widely used for serving static content, load balancing, and handling high traffic websites.

In this complete guide and cheatsheet, I will try to cover everything we need to know to get started with Nginx, from installing the latest version to configuration, security, and performance optimization.

How Nginx Works

At its core, Nginx uses an event-driven, asynchronous architecture that can handle thousands of concurrent connections efficiently. Here's a simplified view of how Nginx fits into a typical web architecture:

Loading diagram...

Nginx can serve static files directly, proxy requests to application servers, load balance across multiple backends, and much more.

Prerequisites

Before we begin, make sure you have:

  • A Linux server running Ubuntu 20.04+ or Debian 11+ (other distros work but commands may differ)
  • Root or sudo access to the server
  • A domain name (optional, but required for SSL certificate sections)
  • Basic familiarity with the command line

Installing Nginx

Let's start by installing nginx on our server. The installation process may vary depending on our operating system. I will cover the installation process for Ubuntu and Debian-based systems.

For other operating systems, please refer to the official Nginx installation guide.

Installing Nginx on Ubuntu/Debian

The version of Nginx available in the default package repositories of Ubuntu and Debian may not be the latest. To install the latest stable version, we can use the official Nginx repository.

For that, we need to install some prerequisite packages first by running the following command:

Code
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
sudo apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring

Next, we need to import the Nginx signing key by running this command:

Code
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg

Now we can add the Nginx repository to our system's package sources by running this command chain:

Code
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

For security we should verify the fingerprint of the Nginx signing key by running the following command before installing Nginx from this source.

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

The output should show a fingerprint that looks like this:

Code
pub   rsa2048 2011-08-19 [SC] [expires: 2027-05-24]
      573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
uid                      nginx signing key <signing-key@nginx.com>

Match the fingerprint 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 with the one provided on the Nginx official website.

If the fingerprints match, it is safe to install Nginx from this repository.

So, now let's update our package list to include the Nginx repository by running:

Code
sudo apt update

And finally, we can install Nginx by running:

Code
sudo apt install nginx

Now we should have the latest stable version of Nginx installed on our system.

We can verify the installation by checking the Nginx version with:

Code
nginx -v

The output should show the installed version of Nginx, for example:

Code
nginx version: nginx/1.29.1

To see the full version information including the modules compiled with Nginx, we can run:

Code
nginx -V

And this should give us a detailed output similar to this:

Code
nginx version: nginx/1.29.1
built by gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04)
built with OpenSSL 3.0.13 30 Jan 2024
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/run/nginx.pid --lock-path=/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_v3_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=/home/builder/debuild/nginx-1.29.1/debian/debuild-base/nginx-1.29.1=. -flto=auto -ffat-lto-objects -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=/home/builder/debuild/nginx-1.29.1/debian/debuild-base/nginx-1.29.1=/usr/src/nginx-1.29.1-1~noble -fPIC' --with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'

Here, we can see we have the HTTP/3 module (--with-http_v3_module) enabled, which we will use later in this guide.

Starting and Enabling Nginx

Now that we have Nginx installed, we need to start the Nginx service and enable it to start on boot.

Code
sudo systemctl start nginx
sudo systemctl enable nginx

We can check the status of the Nginx service by running:

Code
sudo systemctl status nginx

The output should show that the Nginx service is active and running.

Hit Q to exit the status output.

Configuring Firewall for Nginx

For security we should use a firewall to restrict unnecessary access to our server.

Let's configure UFW (Uncomplicated Firewall) to allow HTTP, HTTPS, and HTTP/3 traffic to our server.

Code
sudo ufw enable
sudo ufw allow 'Nginx Full'

Now for HTTP/3 (QUIC), we need to allow UDP traffic on port 443, as HTTP/3 uses UDP instead of TCP.

Code
sudo ufw allow 443/udp

We can check the status of UFW to ensure the rules are applied correctly by running:

Code
sudo ufw status

The output should show that the firewall is active and the rules for Nginx are applied.

Code
Status: active
To                         Action      From
--                         ------      ----
Nginx Full                ALLOW       Anywhere
Nginx Full (v6)           ALLOW       Anywhere (v6)
443/udp                   ALLOW       Anywhere
443/udp (v6)              ALLOW       Anywhere (v6)
... other rules ...

Great! Now our server is ready to serve web traffic securely! You can test it by opening your server's IP address in a web browser. You should see the default Nginx welcome page.

Some Useful Nginx Commands

Before we dive deeper into Nginx configurations, here are some useful commands that we will frequently use with Nginx.

CommandDescription
sudo systemctl start nginxStart the Nginx service
sudo systemctl stop nginxStop the Nginx service
sudo systemctl restart nginxRestart the Nginx service
sudo systemctl reload nginxReload Nginx configuration without downtime
sudo systemctl status nginxCheck the status of the Nginx service
sudo nginx -tTest Nginx configuration for syntax errors
sudo nginx -s reloadReload Nginx configuration
sudo nginx -s stopStop Nginx gracefully
sudo nginx -s quitQuit Nginx gracefully
sudo nginx -vDisplay Nginx version
sudo nginx -VDisplay Nginx version and compile options
sudo tail -f /var/log/nginx/access.logView Nginx access log in real-time
sudo tail -f /var/log/nginx/error.logView Nginx error log in real-time

Here are some important Nginx related files and directories that we should be aware of:

File/DirectoryDescription
/etc/nginx/nginx.confMain Nginx configuration file
/etc/nginx/conf.d/Directory for additional configuration files
/etc/nginx/sites-available/Directory for available server block files
/etc/nginx/sites-enabled/Directory for enabled server block files
/var/www/html/Default web root directory
/var/log/nginx/access.logNginx access log file
/var/log/nginx/error.logNginx error log file

These files and directories are crucial for managing and configuring Nginx effectively.

Configuring Nginx

Now that we have Nginx installed and running, and we are familiar with some useful commands and important files, let's dive into configuring Nginx.

Default Configuration: nginx.conf

When we install Nginx from the official repository, it comes with a very basic default configuration file located at /etc/nginx/nginx.conf.

Now we will replace it with a more optimized and secured one.

Let's first create the directories we need:

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

Next, we will create a backup of the original nginx.conf file and then create a new one:

Code
sudo mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
Code
sudo nano /etc/nginx/nginx.conf

Now, let's add the following configuration to the new nginx.conf file:

Code
user www-data;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65536; # Limit the number of open files
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024; # Increase based on available resources
    multi_accept on;
    use epoll;
    accept_mutex off;
}

http {
    # Basic Settings
    sendfile on;
    sendfile_max_chunk 1m;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 60s;
    keepalive_requests 1000;
    types_hash_max_size 2048;
    server_tokens off;

    # Server Names
    server_names_hash_bucket_size 128;
    server_names_hash_max_size 8192;

    # MIME Types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Buffer Settings
    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    client_header_timeout 30s;
    large_client_header_buffers 4 4k;
    output_buffers 1 32k;
    postpone_output 1460;

    # SSL/TLS Settings (Optimized for modern browsers)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_ecdh_curve X25519:prime256v1:secp384r1;
    ssl_session_timeout 1d;
    ssl_session_cache shared:TLS:10m; # Increase based on available resources
    ssl_session_tickets on;

    # Generate this file with: openssl rand 80 > /etc/nginx/ssl_session_ticket.key
    ssl_session_ticket_key /etc/nginx/ssl_session_ticket.key;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_early_data on;
    ssl_buffer_size 4k;

    # DNS Resolver for OCSP Stapling
    resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Security Headers (Global defaults)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Logging (Optimized with HTTP/3 support)
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" rt=$request_time '
                    'ups_rt=$upstream_response_time ups_addr=$upstream_addr '
                    '"$http_x_forwarded_for"';

    log_format quic '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http3"';

    access_log /var/log/nginx/access.log main buffer=32k flush=5s;
    error_log /var/log/nginx/error.log warn;

    # Compression (Enhanced)
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 1000;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml
        font/truetype
        font/opentype
        application/vnd.ms-fontobject
        application/x-font-ttf
        application/x-font-opentype
        application/x-font-truetype
        image/x-icon
        application/vnd.font-fontforge-sfd;

    # Brotli Compression (uncomment if module available)
    # brotli on;
    # brotli_comp_level 6;
    # brotli_static on;
    # brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # File Cache (Enhanced)
    open_file_cache max=2000 inactive=30s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # Rate Limiting Zones
    limit_req_zone $binary_remote_addr zone=api:1m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=general:1m rate=5r/s;
    limit_req_status 429;

    # Connection Limiting
    limit_conn_zone $binary_remote_addr zone=addr:1m;
    limit_conn_status 429;

    # Map for WebSocket upgrade
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }

    # Cache Configurations
    proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:2m inactive=60m use_temp_path=off max_size=100m;
    proxy_cache_path /var/cache/nginx/static levels=1:2 keys_zone=static_cache:10m inactive=1y use_temp_path=off max_size=1g;

    # Include Configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

This configuration includes several optimizations and security enhancements, such as:

  • Improved SSL/TLS settings for better security and performance.
  • Enhanced logging formats to include more detailed request information.
  • Compression settings for faster content delivery.
  • File caching to reduce disk I/O and improve response times.
  • Rate limiting to protect against abuse and ensure fair usage.
  • Connection limiting to prevent resource exhaustion.
  • WebSocket support for real-time applications.
  • Customizable cache paths for different content types.

After adding the configuration, save and exit the file (by pressing CTRL + X, then Y, and ENTER in nano).

Now let's generate the SSL session ticket key file:

Code
sudo openssl rand -out /etc/nginx/ssl_session_ticket.key 80
sudo chmod 600 /etc/nginx/ssl_session_ticket.key

This key is used for encrypting session tickets, which helps improve the performance of SSL/TLS connections.

Now, let's test the Nginx configuration for any syntax errors by running:

Code
sudo nginx -t

If the output shows syntax is ok and test is successful, we can proceed to reload Nginx to apply the new configuration:

Code
sudo systemctl reload nginx

Now Nginx is configured with a more optimized and secure setup.

This configuration is optimized for low powered (1 CPU, 1-2GB RAM) VMs, if you have more powerful system, you can increase some of the values to get the most out of your machine.

Verifying Gzip Compression

To verify that gzip compression is working correctly, we can use curl with the Accept-Encoding header:

Code
curl -H "Accept-Encoding: gzip" -I https://example.com

If gzip is enabled, we should see Content-Encoding: gzip in the response headers:

Code
HTTP/2 200
content-type: text/html; charset=utf-8
content-encoding: gzip
...

We can also compare the compressed vs uncompressed size:

Code
# Compressed response
curl -H "Accept-Encoding: gzip" -so /dev/null -w '%{size_download}\n' https://example.com

# Uncompressed response
curl -so /dev/null -w '%{size_download}\n' https://example.com

Using Nginx

Now that we have Nginx installed and configured, let's explore how to use it effectively for different use cases.

Hosting a Static Website with Nginx

One of the most common use cases for Nginx is hosting static websites. Let's create a simple server block to serve a static website.

First, let's create a directory for our website files:

Code
sudo mkdir -p /var/www/example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html

Now, let's create a simple index.html file to test our setup:

Code
nano /var/www/example.com/html/index.html

Add some basic HTML content:

Code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to Example.com</title>
</head>
<body>
    <h1>Success! Nginx is serving your website.</h1>
</body>
</html>

Save and exit the file.

Next, let's create a server block configuration for our website:

Code
sudo nano /etc/nginx/sites-available/example.com

Add the following server block configuration:

Code
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html index.htm;

    # Logging
    access_log /var/log/nginx/example.com.access.log main;
    error_log /var/log/nginx/example.com.error.log warn;

    # Security headers (inherited from main config, but can override here)
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Static file caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Main location block
    location / {
        try_files $uri $uri/ =404;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Now, let's enable the site by creating a symbolic link to the sites-enabled directory:

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

Test the Nginx configuration and reload:

Code
sudo nginx -t
sudo systemctl reload nginx

If everything is set up correctly, we should now be able to access our website at http://example.com (assuming DNS is configured properly).

Setting Up SSL/TLS with Let's Encrypt

For production websites, we should always use HTTPS to secure our traffic. Let's Encrypt provides free SSL/TLS certificates that are easy to obtain and renew automatically.

First, let's install Certbot and the Nginx plugin:

Code
sudo apt install certbot python3-certbot-nginx

Now we can obtain an SSL certificate for our domain:

Code
sudo certbot --nginx -d example.com -d www.example.com

Certbot will prompt us to enter an email address for renewal notifications and agree to their terms of service. It will then automatically configure Nginx to use the new certificate.

After Certbot finishes, our server block will be automatically updated to include SSL configuration. However, let's enhance it with a more optimized configuration that includes HTTP/2 and HTTP/3 support.

Let's update our server block:

Code
sudo nano /etc/nginx/sites-available/example.com

Replace the content with this enhanced configuration:

Code
# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    # HTTP/2 support
    http2 on;

    # HTTP/3 (QUIC) support
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html index.htm;

    # SSL certificates (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # HTTP/3 header
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    # Logging with HTTP/3 format
    access_log /var/log/nginx/example.com.access.log quic;
    error_log /var/log/nginx/example.com.error.log warn;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Static file caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Main location block
    location / {
        try_files $uri $uri/ =404;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Test and reload Nginx:

Code
sudo nginx -t
sudo systemctl reload nginx

Automatic Certificate Renewal

Certbot automatically sets up a cron job or systemd timer to renew certificates before they expire. We can test the renewal process with:

Code
sudo certbot renew --dry-run

If the dry run is successful, our certificates will be renewed automatically.

Configuring Nginx as a Reverse Proxy

Nginx excels as a reverse proxy, forwarding client requests to backend servers. This is useful for running Node.js, Python, or other application servers behind Nginx.

Let's create a reverse proxy configuration for an application running on port 3000:

Code
sudo nano /etc/nginx/sites-available/app.example.com

Add the following configuration:

Code
# Upstream configuration
upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;

    server_name app.example.com;

    return 301 https://$host$request_uri;
}

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    listen 443 quic;
    listen [::]:443 quic;

    server_name app.example.com;

    # SSL certificates
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/app.example.com/chain.pem;

    # HTTP/3 header
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    # Logging
    access_log /var/log/nginx/app.example.com.access.log main;
    error_log /var/log/nginx/app.example.com.error.log warn;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Proxy settings
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;

        # Headers
        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;
        proxy_set_header X-Forwarded-Port $server_port;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        proxy_busy_buffers_size 8k;
    }

    # API rate limiting example
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        limit_conn addr 10;

        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        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;
    }

    # Static files served directly by Nginx
    location /static/ {
        alias /var/www/app.example.com/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}

Enable the site and reload Nginx:

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

This configuration includes:

  • Upstream definition with keepalive connections for better performance
  • Full proxy header forwarding for proper application awareness
  • WebSocket support for real-time features
  • Rate limiting on API endpoints
  • Direct static file serving to offload the application server

Load Balancing with Nginx

Nginx can distribute traffic across multiple backend servers for high availability and improved performance.

Here's an example load balancing configuration:

Code
# Load balanced upstream with health checks
upstream app_cluster {
    # Load balancing methods:
    # - round_robin (default): Distributes requests evenly
    # - least_conn: Sends to server with fewest active connections
    # - ip_hash: Routes based on client IP (sticky sessions)
    # - hash: Routes based on a custom key

    least_conn;

    server 192.168.1.10:3000 weight=3;
    server 192.168.1.11:3000 weight=2;
    server 192.168.1.12:3000 weight=1 backup;

    keepalive 64;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    location / {
        proxy_pass http://app_cluster;
        proxy_http_version 1.1;

        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 Connection "";

        # Health check failures
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_next_upstream_timeout 10s;
        proxy_next_upstream_tries 3;
    }
}

The configuration above uses:

  • least_conn: Routes traffic to the server with the fewest active connections
  • weight: Servers with higher weights receive proportionally more traffic
  • backup: Server is only used when primary servers are unavailable
  • proxy_next_upstream: Automatically retries failed requests on other servers

Caching with Nginx

Nginx can cache responses from upstream servers to improve performance and reduce backend load.

Here's how to set up proxy caching:

Code
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # API caching
    location /api/ {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;

        # Enable caching
        proxy_cache api_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;

        # Add cache status header for debugging
        add_header X-Cache-Status $upstream_cache_status;

        # Don't cache requests with these headers
        proxy_cache_bypass $http_cache_control $http_pragma;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Static file caching
    location /static/ {
        proxy_pass http://app_backend;

        proxy_cache static_cache;
        proxy_cache_valid 200 1y;

        add_header X-Cache-Status $upstream_cache_status;
    }
}

The cache zones (api_cache and static_cache) were already defined in our main nginx.conf file earlier.

PHP-FPM Configuration

If we're running PHP applications like WordPress or Laravel, we need to configure Nginx to work with PHP-FPM.

First, let's install PHP-FPM:

Code
sudo apt install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip

Check which PHP-FPM socket is available:

Code
ls /run/php/

The output should show something like php8.3-fpm.sock (version may vary).

Now let's create a server block for a PHP application:

Code
sudo nano /etc/nginx/sites-available/php-app.example.com

Add the following configuration:

Code
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    server_name php-app.example.com;

    root /var/www/php-app.example.com/public;
    index index.php index.html;

    # SSL certificates
    ssl_certificate /etc/letsencrypt/live/php-app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/php-app.example.com/privkey.pem;

    # Logging
    access_log /var/log/nginx/php-app.example.com.access.log main;
    error_log /var/log/nginx/php-app.example.com.error.log warn;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Main location
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP processing
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;

        # PHP-FPM optimizations
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;
        fastcgi_read_timeout 300;

        # Hide PHP version
        fastcgi_hide_header X-Powered-By;
    }

    # Deny access to .htaccess
    location ~ /\.ht {
        deny all;
    }

    # Static file caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}

For WordPress sites specifically, we can add these additional location blocks for better security and performance:

Code
# WordPress specific rules
location = /wp-login.php {
    limit_req zone=general burst=5 nodelay;
    
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

# Disable PHP execution in uploads
location ~* /wp-content/uploads/.*\.php$ {
    deny all;
}

# Protect wp-config.php
location = /wp-config.php {
    deny all;
}

# Block access to sensitive files
location ~* /(wp-config\.php|readme\.html|license\.txt) {
    deny all;
}

Common Redirect Patterns

URL redirects are essential for SEO, domain consolidation, and maintaining link integrity. Here are some common redirect patterns.

Redirect www to non-www (or vice versa)

Code
# Redirect www to non-www
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    
    server_name www.example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    return 301 https://example.com$request_uri;
}

# Redirect non-www to www
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    
    server_name example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    return 301 https://www.example.com$request_uri;
}

Add or Remove Trailing Slashes

Code
# Remove trailing slashes (except for directories)
location ~ ^(.+)/$ {
    return 301 $scheme://$host$1;
}

# Add trailing slashes
rewrite ^([^.]*[^/])$ $1/ permanent;

Redirect Old URLs to New URLs

Code
# Single URL redirect
location = /old-page {
    return 301 /new-page;
}

# Redirect with regex
location ~ ^/blog/(\d{4})/(\d{2})/(.*)$ {
    return 301 /posts/$1-$2-$3;
}

# Redirect entire directory
location /old-section/ {
    return 301 /new-section/;
}

# Map-based redirects for multiple URLs
map $uri $new_uri {
    /old-url-1  /new-url-1;
    /old-url-2  /new-url-2;
    /old-url-3  /new-url-3;
    default     "";
}

server {
    # ... other config ...
    
    if ($new_uri != "") {
        return 301 $new_uri;
    }
}

Redirect HTTP to HTTPS for All Domains

Code
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    
    server_name _;
    
    return 301 https://$host$request_uri;
}

Verifying HTTP/3 Support

After enabling HTTP/3 (QUIC), we should verify that it's working correctly.

Using curl

Modern versions of curl support HTTP/3. We can test with:

Code
curl -I --http3 https://example.com

If HTTP/3 is working, we should see HTTP/3 200 in the response.

Using Browser DevTools

  1. Open the website in Chrome or Firefox
  2. Open DevTools (F12 or right-click → Inspect)
  3. Go to the Network tab
  4. Reload the page
  5. Right-click on the column headers and enable Protocol
  6. Look for h3 or http/3 in the Protocol column

Checking Alt-Svc Header

We can verify the Alt-Svc header is being sent correctly:

Code
curl -I https://example.com | grep -i alt-svc

The output should include something like:

Code
alt-svc: h3=":443"; ma=86400

This tells browsers that HTTP/3 is available on port 443.

Online Testing Tools

We can also use online tools to verify HTTP/3 support:

Security Hardening

Security is paramount when exposing services to the internet. Here are some additional security measures we can implement.

Blocking Common Attacks

Add these location blocks to protect against common attack patterns:

Code
# Block access to sensitive files
location ~* \.(git|env|htaccess|htpasswd|ini|log|sh|sql|bak|swp)$ {
    deny all;
    return 404;
}

# Block PHP execution in uploads directory
location ~* /uploads/.*\.php$ {
    deny all;
    return 404;
}

# Block access to WordPress xmlrpc.php (if applicable)
location = /xmlrpc.php {
    deny all;
    return 404;
}

# Block common vulnerability scanners
location ~* (eval\(|base64_|php://input) {
    deny all;
    return 444;
}

IP-Based Access Control

Restrict access to admin areas or sensitive endpoints:

Code
location /admin/ {
    # Allow only specific IPs
    allow 192.168.1.0/24;
    allow 10.0.0.0/8;
    deny all;

    proxy_pass http://app_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Basic Authentication

Add password protection to sensitive areas:

Code
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin

Then add the authentication to the server block:

Code
location /admin/ {
    auth_basic "Restricted Area";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://app_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

DDoS Protection Basics

Combine rate limiting with connection limiting for basic DDoS protection:

Code
server {
    # ... other config ...

    # Limit connections per IP
    limit_conn addr 100;

    # Limit request rate
    limit_req zone=general burst=50 nodelay;

    location /api/ {
        # Stricter limits for API endpoints
        limit_req zone=api burst=20 nodelay;
        limit_conn addr 20;

        proxy_pass http://app_backend;
    }
}

Monitoring Nginx

Monitoring is essential for understanding server performance and identifying issues before they become critical.

Enabling Status Module

Nginx comes with a built-in stub_status module that provides basic metrics. Add a status endpoint to your server configuration:

Code
# Add this inside a server block (or create a dedicated internal server)
location /nginx_status {
    stub_status on;
    
    # Restrict access to localhost and trusted IPs only
    allow 127.0.0.1;
    allow ::1;
    allow 10.0.0.0/8;
    deny all;
}

Access the endpoint to see metrics:

Code
curl http://localhost/nginx_status

The output shows:

Code
Active connections: 42
server accepts handled requests
 12345 12345 67890
Reading: 0 Writing: 3 Waiting: 39
MetricDescription
Active connectionsCurrent active client connections
acceptsTotal accepted connections
handledTotal handled connections (should equal accepts)
requestsTotal client requests
ReadingConnections reading request header
WritingConnections sending response
WaitingKeep-alive connections waiting for requests

Log Analysis

Regular log analysis helps identify issues and optimize performance.

Useful Log Commands

Code
# Count requests by status code
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# Find top 10 requested URLs
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# Find top 10 client IPs
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# Find slow requests (request time > 1s)
awk '$NF > 1 {print $0}' /var/log/nginx/access.log

# Count requests per hour
awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1,2 | uniq -c

Setting Up Log Rotation

Nginx logs can grow quickly. Ensure logrotate is configured:

Code
cat /etc/logrotate.d/nginx

A typical configuration looks like:

Code
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

Troubleshooting Common Issues

Here are some common Nginx issues and how to resolve them.

Configuration Errors

Always test configuration before reloading:

Code
sudo nginx -t

If there's an error, the output will show the file and line number where the issue was found.

Permission Denied Errors

Check that Nginx has proper permissions to read files:

Code
# Check file permissions
ls -la /var/www/example.com/html/

# Fix ownership if needed
sudo chown -R www-data:www-data /var/www/example.com/html/

# Fix permissions
sudo chmod -R 755 /var/www/example.com/html/

502 Bad Gateway

This usually means the upstream server is not responding:

Code
# Check if the backend is running
sudo systemctl status your-app-service

# Check if the port is listening
sudo ss -tlnp | grep 3000

# Check Nginx error logs
sudo tail -f /var/log/nginx/error.log

504 Gateway Timeout

Increase timeout values for slow backends:

Code
location / {
    proxy_pass http://app_backend;
    
    proxy_connect_timeout 300s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;
}

Checking Logs

Logs are invaluable for troubleshooting:

Code
# View access logs in real-time
sudo tail -f /var/log/nginx/access.log

# View error logs in real-time
sudo tail -f /var/log/nginx/error.log

# Search for specific errors
sudo grep "error" /var/log/nginx/error.log | tail -50

Quick Reference Cheatsheet

Here's a handy reference for common Nginx configurations.

Common Directives

DirectiveDescriptionExample
listenPort to listen onlisten 80;
server_nameDomain namesserver_name example.com;
rootDocument rootroot /var/www/html;
indexDefault index filesindex index.html;
locationURL matchinglocation /api/ { }
proxy_passProxy to backendproxy_pass http://localhost:3000;
try_filesFile fallbacktry_files $uri $uri/ =404;
returnReturn responsereturn 301 https://$host$request_uri;
rewriteURL rewritingrewrite ^/old$ /new permanent;

Location Block Modifiers

ModifierDescriptionPriority
=Exact matchHighest
^~Preferential prefixHigh
~Case-sensitive regexMedium
~*Case-insensitive regexMedium
(none)Prefix matchLowest

Example priority order:

Code
location = /exact { }          # Matches only /exact
location ^~ /static/ { }       # Prefix, stops regex search
location ~ \.php$ { }          # Case-sensitive regex
location ~* \.(jpg|png)$ { }   # Case-insensitive regex
location /prefix/ { }          # Standard prefix match
location / { }                 # Default fallback

Common Variables

VariableDescription
$hostRequest host header
$uriRequest URI without query string
$request_uriFull original request URI
$argsQuery string
$remote_addrClient IP address
$schemeRequest scheme (http/https)
$server_nameServer name
$server_portServer port
$http_*Any HTTP header
$upstream_cache_statusCache hit/miss status

SSL/TLS Best Practices

Code
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

# Session caching
ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
ssl_session_tickets on;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;

Performance Tips

TipConfiguration
Enable Gzipgzip on;
Enable cachingproxy_cache zone_name;
Use keepalivekeepalive_timeout 60s;
Buffer responsesproxy_buffering on;
Use sendfilesendfile on;
Enable HTTP/2http2 on;
Enable HTTP/3listen 443 quic;

Conclusion

Nginx is an incredibly versatile tool that can serve static files, act as a reverse proxy, load balance traffic, and much more. With the configurations and examples in this guide, we should have a solid foundation for deploying and managing Nginx in production environments.

Remember to always:

  • Test configurations before reloading (nginx -t)
  • Use HTTPS for all production websites
  • Keep Nginx and SSL certificates up to date
  • Monitor logs for issues and security threats
  • Implement rate limiting and other security measures

I hope this guide helps you get the most out of Nginx. Happy serving!

Comments

Be the first to share your thoughts!