DevToolBoxGRATIS
Blog

Blok location Nginx dan Panduan Regex

10 menit bacaoleh DevToolBox

The nginx location block is the most critical directive for controlling how Nginx processes incoming requests. It determines which configuration applies to a given URL, enabling you to route traffic, serve files, proxy to backends, and apply security rules with surgical precision. This comprehensive guide covers location block syntax, regex modifiers, matching priority, and 30+ production-ready examples you can copy and adapt immediately.

Generate Nginx configs with our Nginx Config GeneratorTest your regex patterns with our Regex Tester

1. Location Block Syntax

Every location block follows the same fundamental structure: the location keyword, an optional modifier, a pattern to match against the request URI, and a block of directives enclosed in curly braces. The modifier determines how Nginx interprets the pattern -- as a literal prefix, an exact match, or a regular expression.

# Location block syntax
# location [modifier] pattern { directives; }

# 1. Exact match -- highest priority
location = /api/health {
    return 200 "OK";
    add_header Content-Type text/plain;
}

# 2. Prefix priority -- stops regex evaluation
location ^~ /static/ {
    root /var/www;
    expires 30d;
}

# 3. Case-sensitive regex
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

# 4. Case-insensitive regex
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 365d;
    add_header Cache-Control "public, immutable";
}

# 5. Standard prefix (no modifier)
location /api/ {
    proxy_pass http://backend;
}

# 6. Default catch-all
location / {
    try_files $uri $uri/ /index.html;
}

2. Location Modifiers Explained

Nginx provides five ways to match request URIs in location blocks. Understanding these modifiers is the single most important concept for mastering Nginx configuration. Each modifier changes how the pattern is interpreted and affects the priority of the match.

ModifierTypeDescriptionPriority
=ExactExact URI match, stops immediately1 (highest)
^~Prefix priorityPrefix match, skips regex if matched2
~Regex (case-sensitive)PCRE regex, first match in file order wins3
~*Regex (case-insensitive)PCRE regex, case-insensitive3
(none)PrefixLongest prefix match, can be overridden by regex4 (lowest)

= (Exact Match) -- Matches the URI exactly. No more, no less. This is the fastest match because Nginx stops searching immediately when it finds an exact match. Use it for specific paths like the homepage or health check endpoints.

# Exact match: only matches /login, not /login/ or /login?next=/
location = /login {
    proxy_pass http://auth-service:3000;
}

# Exact match for homepage -- fastest possible match
location = / {
    proxy_pass http://homepage-service:3000;
}

~ (Regex, Case-Sensitive) -- Treats the pattern as a PCRE regular expression with case-sensitive matching. Use for file extension matching on case-sensitive filesystems (Linux).

# Case-sensitive regex: matches .php files
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

# Case-sensitive: matches /API/ but not /api/
location ~ ^/API/ {
    proxy_pass http://legacy-api:8080;
}

~* (Regex, Case-Insensitive) -- Treats the pattern as a PCRE regular expression with case-insensitive matching. Preferred for web URLs since clients may use any case.

# Case-insensitive regex: matches .JPG, .jpg, .Jpg, etc.
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
    root /var/www/static;
    expires 365d;
    add_header Cache-Control "public, immutable";
    access_log off;
}

^~ (Prefix Priority) -- A prefix match that, if it matches, prevents Nginx from checking any regex location blocks. Use when you want a prefix match to always win over regex patterns.

# ^~ prefix priority: even if a regex matches /static/..., this wins
location ^~ /static/ {
    alias /var/www/app/static/;
    expires 30d;
    add_header Cache-Control "public";
}

# This regex will NOT override ^~ /static/ above
location ~* \.(css|js)$ {
    expires 7d;
}

(No Modifier / Prefix Match) -- The default behavior. Matches if the URI starts with the given string. However, regex locations can override this match if they also match the URI.

# Prefix match (no modifier): matches /api/users, /api/orders, etc.
# But can be overridden by a regex location
location /api/ {
    proxy_pass http://api-backend:4000;
    proxy_set_header Host $host;
}

# This regex WILL override the prefix match above for .json requests
location ~* \.json$ {
    add_header Content-Type application/json;
    try_files $uri =404;
}

3. Matching Priority Order

When a request comes in, Nginx evaluates location blocks in a specific priority order -- NOT in the order they appear in the config file. Understanding this order prevents countless hours of debugging unexpected routing behavior.

Step 1: Exact match (=) -- Nginx first checks all exact match locations. If one matches, it is used immediately and all other searching stops.

Step 2: Prefix matches (no modifier and ^~) -- Nginx checks all prefix locations and remembers the longest matching prefix. If the longest match has the ^~ modifier, it is used immediately and regex checking is skipped.

Step 3: Regex matches (~ and ~*) -- Nginx checks regex locations in the order they appear in the config file. The first regex that matches wins.

Step 4: Longest prefix fallback -- If no regex matched, the longest prefix match from Step 2 is used.

Key insight: The order of regex blocks in your config file matters, but the order of prefix blocks does not. Nginx always picks the longest prefix match regardless of order.

# Priority demonstration -- request: GET /static/logo.png
# Nginx evaluates in this order:

# Step 1: Check exact matches
location = /static/logo.png { }       # Match? YES -> Use this, STOP
location = /index.html { }            # Not checked (already matched above)

# Step 2: If no exact match, check all prefix locations
location /static/ { }                 # Prefix matches (8 chars)
location /static/logo { }             # Prefix matches (12 chars) -- LONGEST
location ^~ /static/ { }              # If this existed, regex would be skipped

# Step 3: Check regex locations (in config file order)
location ~* \.(png|jpg)$ { }          # Regex matches -> Use this, STOP
location ~* /static/.* { }            # Not checked (first regex already matched)

# Step 4: If no regex matched, use longest prefix from Step 2
# In this case: location /static/logo { }

# ---- Complete example showing priority ----
server {
    listen 80;
    server_name example.com;

    # Priority 1: Exact match (checked first)
    location = / {
        # Only matches GET /
        return 200 "Homepage";
    }

    # Priority 2: ^~ prefix (blocks regex override)
    location ^~ /assets/ {
        # All /assets/* requests come here, no regex can override
        root /var/www;
        expires 30d;
    }

    # Priority 3: Regex (checked in order, first match wins)
    location ~* \.(css|js)$ {
        # Matches .css and .js files (except under /assets/)
        expires 7d;
    }

    location ~ /api/v[0-9]+/ {
        # Matches /api/v1/, /api/v2/, etc.
        proxy_pass http://api-backend;
    }

    # Priority 4: Longest prefix (fallback if no regex matches)
    location /api/ {
        proxy_pass http://default-backend;
    }

    location / {
        # Default catch-all (shortest prefix, lowest priority)
        try_files $uri $uri/ /index.html;
    }
}

4. Regex Patterns for Location Blocks

PCRE (Perl Compatible Regular Expressions) patterns in Nginx location blocks let you match complex URL patterns. Here are the most commonly used patterns with explanations.

# ---- File Extension Patterns ----

# Match image files
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif|bmp|tiff)$ {
    expires 365d;
    add_header Cache-Control "public, immutable";
}

# Match web font files
location ~* \.(woff|woff2|ttf|eot|otf)$ {
    expires 365d;
    add_header Access-Control-Allow-Origin "*";
}

# Match CSS and JavaScript bundles (usually have hash in filename)
location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# Match source maps (block in production)
location ~* \.map$ {
    deny all;
    return 404;
}

# ---- Path-Based Patterns ----

# Match versioned API paths: /api/v1/, /api/v2/, /api/v10/
location ~ ^/api/v[0-9]+/ {
    proxy_pass http://api-backend;
}

# Match UUID in URL: /users/550e8400-e29b-41d4-a716-446655440000
location ~ ^/users/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ {
    proxy_pass http://user-service;
}

# Match numeric IDs: /products/12345
location ~ ^/products/[0-9]+$ {
    proxy_pass http://product-service;
}

# Match language prefixes: /en/, /fr/, /zh/, /ja/
location ~ ^/(en|fr|de|es|zh|ja|ko)/ {
    try_files $uri $uri/ /index.html;
}

# ---- Negative Patterns (deny specific paths) ----

# Block access to hidden files (.git, .env, .htaccess)
location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
}

# Block access to backup files
location ~* \.(bak|old|orig|save|swp|swo)$ {
    deny all;
    return 404;
}

# Block access to configuration files
location ~* /(wp-config|config|configuration)\.php$ {
    deny all;
}

# ---- Query String Matching (using if inside location) ----

# Note: location blocks match URI path only, not query strings.
# Use "if" for query string matching (use sparingly).
location /search {
    # Redirect old search format to new format
    if ($arg_q = "") {
        return 301 /;
    }
    proxy_pass http://search-service;
}

5. The try_files Directive

The try_files directive is one of the most used directives inside location blocks. It tells Nginx to check for files in a specific order and serve the first one found, or fall back to a named location or error code. This is essential for SPAs, static sites with pretty URLs, and hybrid static/dynamic setups.

# ---- try_files syntax ----
# try_files file1 file2 ... fallback;
# Tests each file path in order, serves the first one that exists.
# The last argument is special: it can be a URI (internal redirect)
# or an error code (=404, =500).

# ---- SPA Fallback (React, Vue, Angular) ----
location / {
    root /var/www/app/dist;
    # Try the exact file -> try as directory -> fall back to index.html
    try_files $uri $uri/ /index.html;
}

# ---- Static Site with Pretty URLs ----
location / {
    root /var/www/blog;
    # Try exact file -> try .html extension -> try as directory -> 404
    try_files $uri $uri.html $uri/ =404;
}

# ---- Static Files + Dynamic Backend ----
location / {
    root /var/www/static;
    # Try static file first, then proxy to backend
    try_files $uri $uri/ @backend;
}

# Named location for backend proxy
location @backend {
    proxy_pass http://127.0.0.1:3000;
    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;
}

# ---- Maintenance Mode ----
location / {
    root /var/www;
    # If maintenance.html exists, serve it for all requests
    try_files /maintenance.html $uri $uri/ /index.html;
}

# ---- Multi-language SPA ----
location / {
    root /var/www/app;
    # Try language-specific file, then default, then SPA fallback
    try_files $uri $uri/ /$1/index.html /index.html;
}

# ---- try_files with PHP-FPM ----
location / {
    root /var/www/html;
    index index.php index.html;
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    root /var/www/html;
    fastcgi_pass unix:/run/php/php-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

6. Proxy Pass in Location Blocks

Using proxy_pass inside location blocks is the standard way to route specific URL paths to different backend applications. This is the foundation of microservice routing, API gateways, and backend separation.

# ---- Basic Proxy Pass ----
location /api/ {
    proxy_pass http://127.0.0.1:4000;
    # IMPORTANT: trailing slash behavior
    # proxy_pass http://127.0.0.1:4000;   -> /api/users -> /api/users
    # proxy_pass http://127.0.0.1:4000/;  -> /api/users -> /users (strips /api)
}

# ---- Strip Path Prefix ----
# Request: /api/v1/users -> Backend receives: /users
location /api/v1/ {
    proxy_pass http://backend-v1/;     # Note the trailing slash!
    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;
}

# ---- Upstream Block (Load Balancing) ----
upstream api_cluster {
    least_conn;                         # Send to server with fewest connections
    server 10.0.0.1:4000 weight=5;
    server 10.0.0.2:4000 weight=3;
    server 10.0.0.3:4000 backup;        # Only used when others are down

    keepalive 32;                        # Keep connections alive to backends
}

location /api/ {
    proxy_pass http://api_cluster;
    proxy_http_version 1.1;
    proxy_set_header Connection "";      # Required for keepalive

    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;

    # Timeouts
    proxy_connect_timeout 5s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;

    # Retry on failure
    proxy_next_upstream error timeout http_502 http_503;
    proxy_next_upstream_tries 3;
}

# ---- WebSocket Proxy ----
location /ws/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400s;           # Keep WebSocket alive for 24h
    proxy_send_timeout 86400s;
}

# ---- Microservice Routing ----
location /auth/ {
    proxy_pass http://auth-service:3001/;
}

location /users/ {
    proxy_pass http://user-service:3002/;
}

location /orders/ {
    proxy_pass http://order-service:3003/;
}

location /payments/ {
    proxy_pass http://payment-service:3004/;
}

7. Static File Serving

Nginx excels at serving static files. Understanding the difference between root and alias, and how to set proper cache headers, is essential for performance optimization.

# ---- root vs alias ----

# ROOT: appends full URI to the root path
# Request: /images/photo.jpg -> File: /var/www/images/photo.jpg
location /images/ {
    root /var/www;
    # Nginx looks for: /var/www + /images/photo.jpg
}

# ALIAS: replaces the matched prefix with the alias path
# Request: /images/photo.jpg -> File: /data/photos/photo.jpg
location /images/ {
    alias /data/photos/;
    # Nginx looks for: /data/photos/ + photo.jpg
    # IMPORTANT: alias path MUST end with / when location ends with /
}

# ---- Autoindex (Directory Listing) ----
location /downloads/ {
    alias /var/www/files/;
    autoindex on;
    autoindex_exact_size off;           # Show human-readable sizes (KB, MB)
    autoindex_localtime on;             # Show local time instead of UTC
    autoindex_format html;              # html, xml, json, or jsonp
}

# ---- Cache Headers for Static Assets ----
# Hashed filenames (e.g., app.a1b2c3.js) -- cache forever
location ~* \.[0-9a-f]{8,}\.(css|js)$ {
    root /var/www/app;
    expires max;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# Regular static assets -- cache for 30 days
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
    root /var/www/app;
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
}

# Web fonts -- cache for 1 year, allow cross-origin
location ~* \.(woff|woff2|ttf|eot|otf)$ {
    root /var/www/app;
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin "*";
    access_log off;
}

# HTML files -- short cache, always revalidate
location ~* \.html$ {
    root /var/www/app;
    expires 1h;
    add_header Cache-Control "public, must-revalidate";
}

# ---- Serving Pre-compressed Files ----
location /assets/ {
    root /var/www;

    # Try serving .br (Brotli) first, then .gz (Gzip), then original
    gzip_static on;                      # Serve .gz if it exists
    # brotli_static on;                  # Requires ngx_brotli module

    expires 1y;
    add_header Cache-Control "public, immutable";
}

8. Rewrite Rules in Location Blocks

The rewrite directive modifies the request URI using regex, while return sends an immediate response. Understanding when to use each, and the difference between last, break, permanent, and redirect flags, is critical for URL management.

# ---- return vs rewrite ----
# RETURN: simple, fast, sends immediate response (preferred when possible)
# REWRITE: regex-based URI modification, more powerful but slower

# ---- return examples ----
# 301 Permanent redirect (SEO-friendly, browsers cache this)
location = /old-page {
    return 301 /new-page;
}

# 302 Temporary redirect (not cached by browsers)
location = /promo {
    return 302 /summer-sale;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

# Redirect www to non-www
server {
    listen 443 ssl;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

# Return a custom response body
location /api/maintenance {
    return 503 '{"status":"maintenance","retry_after":3600}';
    add_header Content-Type application/json;
    add_header Retry-After 3600;
}

# ---- rewrite examples ----
# Syntax: rewrite regex replacement [flag];
# Flags: last | break | redirect (302) | permanent (301)

# Rewrite: /blog/2024/my-post -> /posts?year=2024&slug=my-post
location /blog/ {
    rewrite ^/blog/([0-9]{4})/(.+)$ /posts?year=$1&slug=$2 last;
}

# Remove trailing slashes
location ~ ^(.+)/$ {
    rewrite ^(.+)/$ $1 permanent;
}

# Add trailing slashes to directories
location ~ ^/[^.]*[^/]$ {
    rewrite ^(.*)$ $1/ permanent;
}

# Clean URLs: /about -> /about.html
location / {
    rewrite ^/([^.]+)$ /$1.html last;
}

# ---- last vs break ----
# last:  stops current rewrite, restarts location matching with new URI
# break: stops current rewrite, continues processing in current location

# Example demonstrating the difference:
location /download/ {
    # 'last' restarts location search -- may match a different location
    rewrite ^/download/(.*)$ /files/$1 last;
}

location /internal/ {
    # 'break' stays in this location block
    rewrite ^/internal/(.*)$ /private/$1 break;
    root /var/www;
    # Serves /var/www/private/...
}

9. Rate Limiting with Location Blocks

Rate limiting protects specific endpoints from abuse. By applying limit_req inside targeted location blocks, you can set different rate limits for login pages, API endpoints, and static assets.

# ---- Define rate limit zones (in http {} block) ----

# General pages: 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;

# Login/auth endpoints: 5 requests/minute per IP (strict)
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

# API endpoints: 30 requests/second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

# File uploads: 2 requests/second per IP
limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s;

# ---- Apply rate limits in location blocks ----

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

    # Custom 429 error response
    error_page 429 = @rate_limited;
    location @rate_limited {
        default_type application/json;
        return 429 '{"error":"rate_limit_exceeded","message":"Too many requests. Please retry later.","retry_after":60}';
    }

    # General pages with generous burst
    location / {
        limit_req zone=general burst=20 nodelay;
        limit_req_status 429;
        proxy_pass http://frontend:3000;
    }

    # Strict rate limiting for auth endpoints
    location /api/auth/ {
        limit_req zone=auth burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://auth-service:4000;
    }

    # API with moderate burst
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        limit_req_status 429;
        proxy_pass http://api-service:4000;
    }

    # Upload endpoint with strict limits
    location /api/upload {
        limit_req zone=upload burst=5;     # No nodelay: excess requests are delayed
        limit_req_status 429;
        client_max_body_size 100M;
        proxy_pass http://upload-service:4000;
    }

    # No rate limiting for static assets
    location ~* \.(css|js|jpg|png|gif|ico|svg|woff2)$ {
        # No limit_req here -- static assets should always be fast
        root /var/www/static;
        expires 30d;
        access_log off;
    }

    # burst vs nodelay explained:
    # burst=20 nodelay  -> Accept 20 extra requests instantly, reject beyond that
    # burst=20          -> Accept 20 extra requests but delay them to match the rate
    # burst=20 delay=10 -> First 10 excess processed immediately, rest delayed
}

10. Security Headers in Location Blocks

Adding security headers inside location blocks gives you granular control over which headers apply to which paths. This is important because APIs, static assets, and HTML pages often need different security policies.

# ---- Security headers for HTML pages ----
location / {
    proxy_pass http://frontend:3000;

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Block MIME-type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Control referrer information
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Content Security Policy (customize per application)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'self';" always;

    # HSTS: force HTTPS for 2 years
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Permissions Policy
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
}

# ---- Relaxed headers for API endpoints ----
location /api/ {
    proxy_pass http://api-backend:4000;

    # APIs need CORS, not CSP
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

    # CORS headers
    add_header Access-Control-Allow-Origin "https://example.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    add_header Access-Control-Max-Age 86400 always;

    # Handle preflight requests
    if ($request_method = OPTIONS) {
        return 204;
    }
}

# ---- Minimal headers for static assets ----
location ~* \.(css|js|jpg|png|gif|svg|woff2|ico)$ {
    root /var/www/static;
    expires 30d;
    add_header Cache-Control "public, immutable";

    # Allow fonts to be loaded cross-origin
    add_header Access-Control-Allow-Origin "*";

    # No CSP or HSTS needed for static assets
    # (browsers apply the headers from the HTML page)
}

# ---- Block sensitive paths ----
# Block hidden files (.git, .env, .htaccess, .DS_Store)
location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
    return 404;
}

# Block common sensitive files
location ~* /(composer\.json|package\.json|package-lock\.json|yarn\.lock|\.env.*|Makefile|Dockerfile|docker-compose\.ya?ml)$ {
    deny all;
    return 404;
}

11. Common Real-World Patterns

Here are complete, production-tested location block configurations for the most popular frameworks and deployment scenarios.

# ========================================
# WordPress Configuration
# ========================================
server {
    listen 80;
    server_name wordpress.example.com;
    root /var/www/wordpress;
    index index.php;

    # WordPress permalinks
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP processing
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_intercept_errors on;
    }

    # Block access to wp-config.php
    location = /wp-config.php {
        deny all;
    }

    # Block xmlrpc.php (common attack vector)
    location = /xmlrpc.php {
        deny all;
        return 403;
    }

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

# ========================================
# Next.js (Node.js backend) Configuration
# ========================================
server {
    listen 80;
    server_name nextjs.example.com;

    # Proxy everything to Next.js server
    location / {
        proxy_pass http://127.0.0.1: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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Serve Next.js static files directly (/_next/static/)
    location /_next/static/ {
        alias /var/www/nextjs/.next/static/;
        expires 365d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Serve public directory directly
    location /public/ {
        alias /var/www/nextjs/public/;
        expires 30d;
        access_log off;
    }
}

# ========================================
# React SPA (Static Build) Configuration
# ========================================
server {
    listen 80;
    server_name react.example.com;
    root /var/www/react/build;

    # SPA fallback -- all routes go to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache hashed assets forever
    location /static/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Do NOT cache index.html
    location = /index.html {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }

    # API proxy to backend
    location /api/ {
        proxy_pass http://127.0.0.1:4000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# ========================================
# API + Static Files (Microservice Style)
# ========================================
server {
    listen 80;
    server_name app.example.com;

    # Frontend (SPA)
    location / {
        root /var/www/frontend/dist;
        try_files $uri $uri/ /index.html;
    }

    # API v1 -> Service A
    location /api/v1/ {
        proxy_pass http://service-a:3001/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # API v2 -> Service B
    location /api/v2/ {
        proxy_pass http://service-b:3002/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Uploads with size limit
    location /api/upload {
        client_max_body_size 50M;
        proxy_pass http://upload-service:3003;
        proxy_set_header Host $host;
    }

    # WebSocket endpoint
    location /ws {
        proxy_pass http://ws-service:3004;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Shared static assets
    location /assets/ {
        alias /var/www/shared-assets/;
        expires 30d;
        add_header Cache-Control "public";
    }
}

12. Debugging Location Blocks

When your location blocks do not behave as expected, these tools and techniques will help you identify the issue quickly.

# ---- Step 1: Always Test Before Reloading ----
# Check configuration syntax
nginx -t
# Output: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
#         nginx: configuration file /etc/nginx/nginx.conf test is successful

# Test and show the full resolved configuration
nginx -T

# ---- Step 2: Enable Debug Logging ----
# In nginx.conf or server block:
error_log /var/log/nginx/error.log debug;

# Or enable debug for specific IPs only (less performance impact):
events {
    debug_connection 192.168.1.100;
    debug_connection 10.0.0.0/24;
}

# ---- Step 3: Add Debug Headers to Identify Which Location Matched ----
# Add this to each location block during debugging:

location = / {
    add_header X-Debug-Location "exact-root" always;
    # ... your config ...
}

location ^~ /static/ {
    add_header X-Debug-Location "prefix-priority-static" always;
    # ... your config ...
}

location ~* \.(css|js)$ {
    add_header X-Debug-Location "regex-css-js" always;
    # ... your config ...
}

location / {
    add_header X-Debug-Location "default-prefix" always;
    # ... your config ...
}

# ---- Step 4: Test with curl ----
# Check which location handled the request by inspecting headers:
curl -I https://example.com/
curl -I https://example.com/static/style.css
curl -I https://example.com/api/users

# Check response headers:
curl -sI https://example.com/ | grep X-Debug-Location
# Output: X-Debug-Location: exact-root

# Test redirect behavior:
curl -L -v https://example.com/old-page 2>&1 | grep "< HTTP\|< Location"

# ---- Step 5: Monitor Access and Error Logs ----
# Watch access log in real-time:
tail -f /var/log/nginx/access.log

# Filter for specific URI:
tail -f /var/log/nginx/access.log | grep "/api/"

# Watch error log for config issues:
tail -f /var/log/nginx/error.log

# ---- Step 6: Common Issues and Fixes ----

# Issue: "rewrite or internal redirection cycle"
# Cause: location blocks redirect to each other infinitely
# Fix: Check your rewrite rules and try_files for circular references

# Issue: "could not build server_names_hash"
# Fix: Increase hash bucket size in http {} block:
# server_names_hash_bucket_size 128;

# Issue: 403 Forbidden on static files
# Fix: Check file permissions and ensure nginx user can read:
# ls -la /var/www/
# chown -R nginx:nginx /var/www/
# chmod -R 755 /var/www/

# Issue: Regex location not matching
# Debug: Test your regex pattern at regex101.com
# Remember: Nginx uses PCRE, not JavaScript or Python regex
# Remember: Backslashes must be escaped in nginx.conf: \. not .

# ---- Reload After Fixing ----
nginx -t && nginx -s reload

Frequently Asked Questions

What is the difference between location = / and location / in Nginx?

"location = /" matches ONLY the exact root path (/), nothing else. It is the fastest match. "location /" is a prefix match that matches every URI since all URIs start with /. It acts as the default catch-all location. In practice, use "location = /" for homepage-specific config and "location /" as the fallback for everything else.

Why does my regex location block not match? How does Nginx location priority work?

Nginx evaluates locations in this strict order: (1) exact match = stops immediately, (2) longest prefix match with ^~ stops immediately, (3) regex blocks ~ and ~* are checked in config file order and the first match wins, (4) if no regex matches, the longest prefix match is used. If your regex does not match, check if a ^~ prefix block is intercepting the request first, or if another regex appearing earlier in the file is matching instead.

What is the difference between root and alias in Nginx location blocks?

With "root", Nginx appends the full URI to the root path. So "location /images/ { root /data; }" serves /data/images/photo.jpg for the URI /images/photo.jpg. With "alias", Nginx replaces the matched location prefix with the alias path. So "location /images/ { alias /data/photos/; }" serves /data/photos/photo.jpg for the same URI. Use alias when the filesystem path does not mirror the URL structure.

How do I make Nginx location blocks work with a React or Next.js SPA?

For SPAs, you need try_files to fall back to index.html for client-side routing: "location / { try_files $uri $uri/ /index.html; }". For Next.js with a Node.js backend, proxy all requests: "location / { proxy_pass http://localhost:3000; }". For Next.js static export, use the SPA pattern. Add separate location blocks for API routes and static assets with appropriate caching headers.

Can I use multiple regex patterns in a single Nginx location block?

No, each location block takes exactly one pattern. However, you can use regex alternation (the | operator) within a single regex pattern to match multiple patterns: "location ~* \.(jpg|png|gif|webp)$ { ... }". This matches any URI ending in .jpg, .png, .gif, or .webp. For completely different patterns, create separate location blocks.

Mastering Nginx location blocks is the key to building fast, secure, and maintainable web server configurations. Start with exact and prefix matches for simple cases, use regex only when needed, and always test with "nginx -t" before reloading. Use the tools below to generate and test your configurations.

Generate Nginx configs with our Nginx Config GeneratorTest your regex patterns with our Regex Tester
𝕏 Twitterin LinkedIn
Apakah ini membantu?

Tetap Update

Dapatkan tips dev mingguan dan tool baru.

Tanpa spam. Berhenti kapan saja.

Coba Alat Terkait

NXNginx Config Generator.*Regex Tester.ht.htaccess Generator

Artikel Terkait

Contoh Konfigurasi Nginx: Reverse Proxy, SSL, dan Situs Statis

Konfigurasi Nginx siap produksi: reverse proxy, SSL/TLS, file statis, load balancing.

Regex Cheat Sheet: Referensi Lengkap Regular Expression

Cheat sheet regex lengkap: sintaks, kelas karakter, quantifier, lookahead, dan pola siap pakai.