How to Redirect URLs in Nginx

How to Redirect URLs in Nginx

How to Redirect URLs in Nginx

URL redirects seem like they should be trivial, and most of the time they are — until you accidentally create a redirect loop at 2 AM during a domain migration and watch your site go completely unreachable while your browser cheerfully reports “too many redirects.” I’ve done this. More than once. This guide is everything I’ve learned about doing Nginx redirects correctly, including the mistakes that taught me the hard way.

Understanding return vs rewrite

Nginx gives you two main tools for redirects, and knowing when to use each one matters:

My rule of thumb, which matches Nginx’s own documentation advice: prefer return whenever possible. It’s evaluated earlier in Nginx’s request processing and is more predictable. Reach for rewrite only when you need regex capture groups or more complex logic.

301 vs 302: Which One Do You Actually Want?

This distinction trips up more people than anything else in this guide, so let’s be precise:

I’ve seen real SEO damage from using 302s for what were actually permanent moves — search engines kept indexing the old URL for months because they were told, explicitly, not to treat the move as permanent. Get this right; it matters more than it seems like it should.

Basic Redirect Syntax

The simplest form, redirecting one specific URL to another:

server {
    listen 80;
    server_name example.com;

    location = /old-page {
        return 301 /new-page;
    }
}

The = in location = /old-page means an exact match — this only fires for that precise path, not for /old-page/anything-else.

Redirecting an Entire Domain (or Subdomain) to Another

This is one of the most common real-world cases — migrating from oldsite.com to newsite.com, or from a non-www to www version of your domain (or vice versa).

server {
    listen 80;
    listen 443 ssl;
    server_name oldsite.com www.oldsite.com;

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

    return 301 https://newsite.com$request_uri;
}

$request_uri is important here — it preserves the original path and query string, so oldsite.com/blog/my-post?ref=twitter correctly redirects to newsite.com/blog/my-post?ref=twitter instead of dropping everything after the domain.

Redirecting HTTP to HTTPS

This is a redirect nearly every production site needs, and it’s simple:

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

server {
    listen 443 ssl;
    server_name example.com www.example.com;

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

    # your actual site config here
}

I use $host here instead of hardcoding the domain name, so this same block works correctly for both example.com and www.example.com without needing to duplicate it. If you’re using Certbot, it typically sets this up for you automatically when you run certbot --nginx.

Redirecting www to non-www (or the Reverse)

Pick one canonical version of your domain and stick to it — having both example.com and www.example.com serving identical content without a redirect between them is a duplicate-content problem for SEO.

Redirecting www to non-www:

server {
    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;
}

Redirecting non-www to www is just the mirror image — swap which server_name gets the redirect and which one is canonical.

Using rewrite for Pattern-Based Redirects

Sometimes you need to redirect based on a pattern rather than an exact path. This is where rewrite earns its place. Say you’re migrating from /blog/2023/my-post to /articles/my-post — dropping the year segment across every old blog post:

location /blog/ {
    rewrite ^/blog/[0-9]{4}/(.*)$ /articles/$1 permanent;
}

permanent here is shorthand for a 301 redirect (there’s also redirect, which issues a 302). The (.*)$ captures everything after the year, and $1 in the destination reuses that captured value.

I’ll be honest: I reach for return with map blocks for anything beyond the simplest regex pattern, because deeply nested rewrite rules get hard to reason about and even harder to debug six months later. For example, redirecting a specific list of old paths to new ones using a map:

map $uri $new_uri {
    /old-about      /about;
    /old-contact    /contact;
    /old-pricing    /pricing;
}

server {
    listen 80;
    server_name example.com;

    if ($new_uri) {
        return 301 $new_uri;
    }
}

This scales much better than a pile of individual location blocks when you have dozens of one-off redirects to manage, like after a site restructuring.

Redirecting a Whole Path Prefix

If you moved an entire section of your site — say /shop/ became /store/ — and want every URL underneath it preserved:

location /shop/ {
    rewrite ^/shop/(.*)$ /store/$1 permanent;
}

This correctly turns /shop/shoes/running into /store/shoes/running, preserving everything after the prefix.

Avoiding Redirect Loops

This is the mistake I mentioned at the start, and it’s worth dwelling on because it’s so easy to create by accident. A redirect loop happens when a redirect rule’s destination also matches the same rule (directly, or indirectly through another redirect that points back).

A classic way to accidentally create one: combining an HTTP→HTTPS redirect with a www→non-www redirect in the wrong order, or with conflicting server_name blocks, so the browser gets bounced between the two rules indefinitely.

To avoid this, I always:

  1. Test redirect chains manually with curl -IL (the -L follows redirects, -I shows headers only), so I can see the entire chain of hops before deploying
  2. Keep redirect logic in as few places as possible — ideally, exactly one canonical redirect chain: HTTP → HTTPS → canonical domain, in that order, never circular
  3. Never redirect a URL to itself, even conditionally — double check any map or rewrite output against its own input

Testing a redirect chain:

curl -IL http://www.oldsite.com/some-page

This shows every hop, in order, with status codes — if you see the same URL appear twice, you’ve got a loop.

Testing Your Redirects

  1. sudo nginx -t before every reload, always
  2. curl -I https://example.com/old-page — confirm the Location header points where you expect and the status code is what you intended (301 vs 302)
  3. curl -IL https://example.com/old-page — follow the full chain and make sure it terminates in a 200, not a loop
  4. Test with query strings attached, to confirm $request_uri (or $is_args$args if you’re constructing the URL manually) is preserving them correctly
  5. Test in an actual browser too — some redirect issues (like mixed content warnings after HTTP→HTTPS redirects) only show up there

Troubleshooting Common Issues

“Too many redirects” in the browser — You have a loop. Trace it with curl -IL and look for a repeating pattern in the hops.

Query strings getting dropped — You likely used a hardcoded destination instead of appending $request_uri or $is_args$args.

Redirect not firing at all — Check location block specificity; a more specific block elsewhere in your config might be matching first. Nginx’s location matching order (exact match, then longest prefix match, then regex in the order they appear) is worth reviewing if a redirect seems to be silently ignored.

Search engines still showing old URLs months later — Confirm you used 301, not 302. Also confirm the redirect has actually been live and consistent — search engines need to consistently see the 301 across multiple recrawls before fully updating their index.

Mixed content warnings after HTTP→HTTPS redirect — This isn’t an Nginx issue; it means your HTML is hardcoding http:// URLs for internal assets. Fix these in your application code to use protocol-relative or HTTPS URLs.

Security Considerations

Performance Tips

Real-World Use Case

During a domain rebrand, I had to redirect an entire old domain, preserve full paths and query strings, force HTTPS, and canonicalize away from www — all without breaking existing inbound links from years of accumulated backlinks and bookmarks. The final setup was: one server block on the old domain catching both HTTP and HTTPS, issuing a single 301 straight to the new canonical HTTPS non-www URL with $request_uri appended, avoiding any intermediate hops. Testing with curl -IL before going live caught an early version that had an accidental loop between the www and non-www rules — a five-minute test that saved what would have been a very bad launch day.

Best Practices Recap

Redirects are one of those Nginx features that are simple in isolation and genuinely dangerous in combination — each individual rule usually makes sense on its own, but chains of them interacting is where loops and dropped query strings sneak in. Testing the full chain with curl -IL, every single time, is the one habit that’s saved me the most grief.

Exit mobile version