Skip to main content

Command Palette

Search for a command to run...

NGINX Intermediate Config (Part 2)

Updated
7 min readView as Markdown

URL Manipulation — Mastering Redirects and Rewrites

Welcome to Chapter 2 of our Intermediate Config module! In our last guide, we looked at how to split a single NGINX web server instance into multiple independent virtual hosts. But as production environments grow, web architectures change.

Domains get migrated, folders get reorganized, and security mandates require us to force unencrypted HTTP sessions onto secure HTTPS channels. To manage these shifts seamlessly without breaking user bookmarks or losing SEO ranking value, an engineer must master NGINX's two primary traffic-shaping mechanisms: Redirects (return) and Rewrites (rewrite).

In this guide, we will break down the structural differences between them, look at production-grade config templates, and review regular expressions to manipulate URIs on the fly.

1. Architectural Blueprint: Redirect vs. Rewrite

While both directives manipulate how requests are resolved, they operate at completely different stages of the client-server lifecycle.

  • Redirect (return): This is a client-side operation. When NGINX hits a return block, it immediately halts processing and sends an HTTP status code (like 301 Moved Permanently) back to the browser. The browser sees this code, updates its address bar to show the new URL, and initiates a brand new connection to the updated location.

  • Rewrite (rewrite): This is primarily a server-side operation. NGINX modifies the requested URI internally using regular expressions before passing it down to file checks or upstream proxy locations. The visitor's browser address bar stays exactly the same, completely hiding your internal directory renames or backend application structures.

2. Real-World Routing via the return Directive

The return directive is highly efficient because it stops processing immediately once matched, saving CPU cycles. Here are the three most common production configurations.

Use Case A: Entire Domain Migration

When shifting branding from an old domain over to a clean destination, use a permanent redirect to pass your search engine optimization (SEO) history along:

server {    
    listen       80;    
    server_name  honda.cars.com;    
    
    # Send all requests permanently to the new domain structure
    return 301 https://cars.honda.com$request_uri;    
}
  • $request_uri: This built-in variable captures the exact sub-path and parameters the user typed (e.g., /civic?color=red). This ensures that visitors land on the exact matching page of the new site rather than getting dumped on the homepage.

Use Case B: Forcing HTTP to HTTPS

Security baselines require all production applications to run over TLS/SSL. Here is how you can intercept unencrypted port 80 traffic and upgrade it:

server {    
    listen       80;    
    server_name  diner.com;    
    
    # Force connection upgrade
    return 301 https://\(host\)request_uri;
}

server {    
    listen       443 ssl;    
    server_name  diner.com;    
    
    ssl_certificate     /etc/ssl/certs/diner.com.pem;    
    ssl_certificate_key /etc/ssl/certs/diner.com-key.pem;    
    root  /var/www/diner;    
}

Use Case C: Single-Page Redirects

If you are deprecating or updating a single high-traffic landing page, you can nest the return directive straight inside a specific location block:

location /civic-type-r {        
    return 301 https://cars.honda.com/type-r;    
}

3. Mastering the rewrite Directive with Regular Expressions

When you need to alter incoming paths dynamically without changing what appears in the user's browser, you need the rewrite directive. It uses standard Regular Expressions (regex) to match and capture path fragments.

Quick Regex Reference Cheat Sheet

Regex Token

Evaluation Purpose

Example Layout

^

Indicates the strict start of a string path.

^/old matches /old-path

\(

Indicates the strict end of a string path.

/page\) matches /my/page

.

Wildcard: Matches any single character token.

a.b matches acb or axb

*

Matches zero or more repetitions of the previous token.

a* matches a or aaa

(.*)

Capture Group: Snatches everything and stores it in a temporary variable (\(1, \)2, etc.).

^/images/(.*)$ captures the filename

Imagine your application asset folder was renamed on disk from /images to /pics. You want old bookmarked image links to keep working without throwing a 404 error.

server {    
    listen 80;    
    server_name example.com;    
    root /var/www/html;    

    location / {        
        # Capture anything following /images/ and inject it into $1
        rewrite ^/images/(.*)\( /pics/\)1 permanent;        
        
        try_files \(uri \)uri/ =404;    
    }
}

Breaking Down the Mechanics:

  1. The regex pattern ^/images/(.*)$ instructs NGINX to look for paths starting with /images/.

  2. The parenthesis (.*) act as a capture group, capturing the rest of the filename (e.g., pic01.jpg).

  3. NGINX places that captured text value straight into a temporary system token named $1.

  4. The destination parameter maps this token onto the new target path: /pics/$1.

  5. The trailing permanent flag commands NGINX to return an official HTTP 301 back to the browser, updating the client's search indexes automatically.

4. Common HTTP Routing Status Codes

When debugging your rewrite and redirect configurations, always keep your browser's Developer Tools network tab open to track response codes:

Code

Standard Core Meaning

Operational SRE Takeaway

200

OK

Request processed and delivered successfully.

301

Moved Permanently

Permanent redirect; transfers search engine ranking history.

302

Found

Temporary redirect; tells browsers not to cache the redirection.

404

Not Found

The requested path or file does not exist on disk.

502

Bad Gateway

NGINX is working fine, but the upstream backend application server crashed or timed out.

5. 'last' flag V/S the 'break' flag

This part of NGINX configuration trips up almost everyone when they start writing intermediate routing rules.

Both last and break tell NGINX to stop executing any further rewrite lines below them in that block. The critical, structural difference is what NGINX does with the newly modified URI right afterward.

The Apartment Analogy

To build on our apartment building analogy from the previous chapter, imagine a visitor walks into a specific apartment (location block) with their paperwork (the requested URI). Inside that room, a rewrite rule alters their paperwork.

  • last tells the visitor: "Your paperwork has completely changed. Please leave this room immediately, go back down to the building lobby, read the directory map on the wall from the very top, and find the new apartment that matches your updated paperwork."

  • break tells the visitor: "Your paperwork has changed, but stay right here in this room. We are going to finish processing your request using only the tools and files available inside this exact apartment."

Code Examples: Seeing the Behavior in Action

Let’s look at two scenarios to see how this affects your system's behavior.

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    location /old-downloads {
        # 1. Matches here first
        # 2. Changes URI to /v2-downloads/archive.zip
        # 3. Kicks request out to start matching locations from the top again
        rewrite ^/old-downloads/(.*)\( /v2-downloads/\)1 last;
    }

    location /v2-downloads {
        # 4. Request lands here safely because of the restart!
        try_files $uri =404;
    }
}

Because we used last, NGINX picks up the rewritten path (/v2-downloads/archive.zip), runs it through the location selection algorithm from scratch, matches the second location /v2-downloads block, and serves the file.

Scenario 2: Using break (Staying in Place)

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    location /old-downloads {
        # 1. Matches here first
        # 2. Changes URI to /v2-downloads/archive.zip
        # 3. Stops rewrites, but stays trapped in THIS location block
        rewrite ^/old-downloads/(.*)\( /v2-downloads/\)1 break;

        # 4. NGINX looks for: /var/www/html/old-downloads/v2-downloads/archive.zip
        try_files $uri =404; 
    }

    location /v2-downloads {
        # NGINX never gets here!
        try_files $uri =404;
    }
}

Because we used break, NGINX refuses to look for a new location block. It attempts to serve the request using the root and try_files directives inside the current block. It combines the original location prefix with the new path, resulting in a broken file path check and a 404 error.

Quick Reference Summary

Architectural Behavior

last

break

Stops further rewrites in current block?

Yes

Yes

Triggers Location Re-evaluation?

Yes (Goes back to the lobby)

No (Stays in the room)

Best Used For...

Public-facing URL changes where the destination needs to be evaluated by a different location block.

Internal path corrections or asset lookups where the current block already points to the correct filesystem root.