Nginx Configuration Basics -- Reverse Proxy, SSL, and Common Setups

Learn essential Nginx configuration for reverse proxy, SSL termination, static file serving, and common web server setups.

Nginx has become the go-to web server for modern applications, powering some of the world's busiest websites. Its lightweight architecture and efficient request handling make it ideal for everything from reverse proxying to serving static content. Whether you're setting up a simple website or a complex microservices infrastructure, understanding Nginx configuration is essential.

This guide covers the fundamentals you need to deploy Nginx in production, with practical examples you can adapt to your specific needs.

Understanding Nginx Configuration Structure

Nginx configuration files are plain text files that define how the server behaves. The main configuration file is typically located at /etc/nginx/nginx.conf, and it uses a hierarchical block structure.

The basic hierarchy looks like this:

main context (global settings)
  |
  +-- events context (connection processing)
  |
  +-- http context (HTTP protocol settings)
      |
      +-- server blocks (virtual hosts)
          |
          +-- location blocks (request routing)

Each block contains directives (setting = value pairs) that control specific behaviors. Let's explore each level.

The Server Block: Your Virtual Host

A server block defines a virtual host that listens for requests on specific ports and domain names. This is where most of your configuration lives.

Here's a minimal server block:

server {
    listen 80;
    server_name example.com www.example.com;

    location / {
        root /var/www/html;
        index index.html;
    }
}

The listen directive tells Nginx which port to accept connections on. The server_name directive matches incoming requests to the Host header in HTTP requests. You can specify multiple domain names, and even use wildcards like *.example.com.

If you need to listen on IPv6, add another listen directive: listen [::]:80;. For multiple server blocks on different ports, each can have its own configuration.

Location Blocks: Routing Requests

Location blocks determine how Nginx handles requests to specific URL paths. They use prefix matching by default, but also support regular expressions.

location / {
    # Handle all requests
}

location /api/ {
    # Handle requests starting with /api/
}

location ~ \.jpg$ {
    # Handle requests ending in .jpg (regex)
}

location ^~ /static/ {
    # Exact prefix match, stop searching for more specific locations
}

The ^~ prefix means "if this matches, stop looking for more specific patterns." The ~ indicates a case-sensitive regex, and ~* means case-insensitive. Exact matches take highest priority, followed by longest prefix matches, then regex matches.

Reverse Proxying to Application Servers

One of Nginx's most powerful features is reverse proxying. Instead of running your application directly in Nginx, you run it on another port and have Nginx forward requests to it.

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://localhost:3000;
        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_buffering off;
    }
}

The proxy_pass directive tells Nginx where to forward the request. The additional proxy_set_header directives are crucial:

  • Host preserves the original domain
  • X-Real-IP passes the client's actual IP address
  • X-Forwarded-For maintains a chain of IPs through proxies
  • X-Forwarded-Proto indicates whether the original request was HTTP or HTTPS

These headers are essential for your application to know the real client IP and original protocol, especially when behind a proxy.

For upstream services, you can define upstream blocks for connection pooling and load balancing:

upstream app_servers {
    server localhost:3000;
    server localhost:3001;
    server localhost:3002;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://app_servers;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Nginx will round-robin requests among the listed servers by default. Add weight parameters for weighted load balancing: server localhost:3000 weight=5;

SSL/TLS Configuration

Securing traffic with HTTPS is non-negotiable for modern web applications. Nginx makes SSL setup straightforward.

server {
    listen 443 ssl http2;
    server_name secure.example.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header X-Forwarded-Proto https;
    }
}

server {
    listen 80;
    server_name secure.example.com;
    return 301 https://$server_name$request_uri;
}

The first block handles HTTPS traffic. The ssl_certificate and ssl_certificate_key point to your certificate files. The ssl_protocols line enforces modern TLS versions (avoid SSLv3 and TLSv1.0).

The second block handles HTTP traffic by redirecting all requests to HTTPS using a 301 permanent redirect. This ensures users always connect securely.

For free certificates, use Let's Encrypt with Certbot:

certbot certonly --standalone -d example.com -d www.example.com

Certbot places certificates in /etc/letsencrypt/live/example.com/ for automatic renewal.

Serving Static Files

Nginx excels at serving static content. Configure a location block for static assets:

server {
    listen 80;
    server_name example.com;

    root /var/www/html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

The root directive sets the base directory for file lookups. The try_files directive is useful for single-page applications: it tries the requested file, then a directory, then falls back to index.html.

For static assets, set long expiration times. The ~* location uses case-insensitive regex matching for file extensions. The expires directive tells browsers to cache for 30 days, reducing server load.

Compression with Gzip

Compressing responses can dramatically reduce bandwidth usage:

gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript
            application/x-javascript application/xml+rss
            application/javascript application/json;
gzip_disable "MSIE [1-6]\.";

Place this in the http context to enable gzip globally. The gzip_min_length prevents compression of tiny responses (overhead isn't worth it). The gzip_types list specifies which content types to compress. Skip images and other pre-compressed formats.

Rate Limiting

Protect your application from abuse with rate limiting:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://localhost:3000;
    }

    location /login {
        limit_req zone=login_limit burst=5;
        proxy_pass http://localhost:3000;
    }
}

Define rate limit zones in the http context. The first parameter is the key (using $binary_remote_addr limits per IP). Set the zone name, memory size (10m), and rate (requests per second or minute).

In location blocks, apply the zone. The burst parameter allows temporary exceeding of the rate limit. The nodelay keyword immediately drops requests exceeding the burst.

Common Configuration Mistakes

Avoid these pitfalls:

  1. Forgetting the trailing slash in proxy_pass: proxy_pass http://localhost:3000/; behaves differently than without the slash.

  2. Not setting required headers: Always include X-Real-IP and X-Forwarded-For when proxying. Your application relies on these.

  3. Using regex locations carelessly: Regex locations are evaluated in order, making configuration harder to maintain. Prefer prefix matching.

  4. Excessive buffering: For streaming responses, set proxy_buffering off; to avoid buffering the entire response in memory.

  5. Ignoring log files: Check /var/log/nginx/error.log and /var/log/nginx/access.log for configuration issues and debugging.

Testing and Reloading Configuration

Always validate your configuration before reloading:

nginx -t
sudo nginx -s reload

The -t flag tests syntax without reloading. The reload signal gracefully restarts Nginx without dropping connections.

Conclusion

Nginx configuration seems complex at first, but once you understand the hierarchical structure and key directives, building production-ready configurations becomes straightforward. Start simple with basic server and location blocks, then add reverse proxying, SSL, and optimization directives as needed.

The key is understanding that Nginx sits between clients and your application, handling connection management, SSL termination, and request routing efficiently. With the patterns shown here, you can handle everything from simple static sites to complex distributed systems.

Keep your configurations organized, test thoroughly, and monitor logs to catch issues early. Your future self will thank you when something goes wrong at 2 AM and you need to debug quickly.

Related Tools