How to do an Nginx redirect

Nginx is an extremely efficient and quite flexible web server. When you want to do a redirect in Nginx, you have a few options to select from, so you can choose the one that suits you best to do an Nginx redirect.

Simplest and fastest: return

The by far simplest and fastest – because there is no regexp that has to be evaluated – is to use the return statement. Place this in your server block:

return 301 https://example.com$request_uri;

This is a permanent redirect. Use 302 if you want a temporary redirect. A full sample server block could be:

server {
    listen 80;
    listen [::]:80;
    hostname example.com www.example.com;
    return 301 https://example.com$request_uri;
}

Using regular expressions

If you need something more complex, don’t be afraid to use a regexp – they’re still extremely fast in Nginx:

rewrite ^/foo/(bar)/(.*)$ https://$server_name/$1/$2 permanent;

Replace permanent with redirect if you want a temporary (HTTP 302) redirect. Our full sample server block could then be:

server {
    listen 80;
    listen [::]:80;
    hostname example.com www.example.com;
    root /var/www/example.com/public;
    rewrite ^/foo/(bar)/(.*)$ $scheme://$server_name/$1/$2 permanent;
}

Using maps

If you have a list of URLs or regular expressions that you want to redirect differently, you ought to look into using a map, which you very well may define in a separate file for your convenience. Just note that the map definition must be outside the server block:

include redirect-map.conf;
server {
    […]
    if ( $redirect_uri ) {
        return 301 $redirect_uri;
    }
}

The file redirect-map.conf may then look something like this:

map $request_uri $redirect_uri {
    /about.html          /about-us;
    /customers.html      /our-customers;
    /products.html       /our-products;
}

Note the following excerpt from the docs:

A regular expression should either start from the “~” symbol for a case-sensitive matching, or from the “~*” symbols (1.0.4) for case-insensitive matching. A regular expression can contain named and positional captures that can later be used in other directives along with the resulting variable.

Here’s an example that might help you understand what that was about:

map $request_uri $redirect_uri {
    /about.html          /about-us;
    /customers.html      /our-customers;
    /products.html       /our-products;
    # Match any url that ends in products.html or producs.htm
    ~products\.html?$    /our-products;
    # case-insensitive version of the above
    ~*products\.html?$   /our-products;
    # A named capture that maps
    # e.g. product-1234.html into /products/item-1234/overview
    ~product-(?<sku>\d+)\.html   /products/item-$sku/overview;
}

Cute, huh? :-) Also please note that the variable name $redirect_uri have no special meaning: It is one I made up. You can name it whatever you like, but make sure the variable name in the map and the server block matches.

Some useful variables

I’ve used a few of these in the examples above, so you might have noticed them before. These are variables that comes predefined by Nginx, ready for you to use in your configs:
$scheme – The scheme used for the current request. E.g. “http” or “https”
$host – The hostname provided by the client for the current request.
$server_name – The first hostname from the hostname declaration in your config for the server block that responds to the request.
$request_uri – The full original request URI – with arguments.
$request_filename – The file path for the current request.

Here’s also the full list of predefined variables available in your Nginx config.

Some useful recipes for an Nginx redirect

HTTP to HTTPS

return 301 https://$host$request_uri;

Read more about HTTP to HTTPS redirects in Nginx here.

Canonical hostname

If the hostname doesn’t match the first name in the server_name list. Makes sure your content is only available at the canonical hostname, e.g. to avoid duplicate content issues. Excellent for redirecting non-www to www or redirecting www to non-www in Nginx as long as your server block is only for a single website.

server_name example.com www.example.com example.net www.example.net _;
if ( $host != $server_name ) {
    return 301 $scheme://$server_name$request_uri;
}

Nginx is very efficient, but please note that it would be more efficient to have two separate server blocks – one for the hostnames you want to redirect and one for the website. Then Nginx won’t have to do the comparison for every request.

Generic non-www/www redirects

If your server block covers multiple websites – e.g. a WordPress multisite network and you don’t want all of them to redirect to the same hostname, you can still do a universal check:

Redirect non-www to www:

if ( $host !~ ^www\. ) {
    return 301 $scheme://www.$host$request_uri;
}

Redirect www to non-www

if ( $host ~ ^www\.(?<domain>.+)$ ) {
    return 301 $scheme://$domain$request_uri;
}

For the www/non-www redirects, it is worth mentioning again that using separate server blocks, where one use the return statement described at the top, is by far the most efficient.

34 Comments

  1. what if I need to handle redirect from other domain name preserving original url in search bar?

    eg:

    if ($host == ‘some_partner_host_that_redirects_to_my_page’) {
    rewrite ^ some_partner_host_that_redirects_to_my_page$request_uri permanent;
    }

    is that even possible to detect redirect origin?

    thnks in advance

    1. If the HTTP header “Referer” is passed, you can check for $http_referer.

  2. Thanks for the info. How can you handle spaces in the original url in the redirect map?

    I tried:
    ‘/banner ads.htm’ /links/link-humor-times/;

    …but that’s not working.

    Also, it’s weird, but some of my other redirects aren’t working, some are. For example, this one is not for some odd reason, and I know the new link is good:

    /cartoons/political-toons/ /topics/cartoons/political-toons/;

    I disabled the cache plugin and paused Cloudflare, and flushed the browser cache several times, so I don’t think it’s a cache issue.

    What can explain this behavior? Thank you.

    1. Regular double quotes should do it:
      "/banner ads.htm" /links/link-humor-times/;

  3. How about if I want to include in the map a pattern like this:

    /example/one-word/ and replace it for /green/one-word/

    I tried

    /example/[a-z]/$ /green/$1; but it did not work
    /example/?/$ /green/$test; but it did not work

      1. You need to add:
        volatile; to the start of the map. Ex:

        map $request_uri $redirect_uri {
        volatile;
        /index.html /index;
        /index2.html /index2;

  4. Bjørn,

    I created the map and everything works well.

    Now, I would like to get rid off all the trailing slashes:

    I am using this inside the server definition and outside of location

    if (!-e $request_filename) {
    rewrite ^/(.*)/$ /$1 permanent;
    }

    It works well, but when it comes to directories it just creates a infinite loop. Any suggestions on how to get rid off the trailing slash with nginx.

    Thank you,

    Lex

    1. Because of the link, I assume that with “this” you mean the two parts where I use ifs and explains that using separate server blocks is by far the most efficient. I fail to see the point of your comment. Is it to prove that you are able to read documentation, but not blog posts before posting comments?

      Oh, btw: It is not wrong. It works fine. As intented. It is not likely to break anytime. The statement that it is wrong, is objectively wrong.
      It might be less cumbersome than maintaining two server blocks. The statement that it is cumbersome is subjective.
      Yes, it is less efficient when Nginx runs. To most people it won’t matter.

  5. Hello Björn,
    Thank you for your suggestion!
    I have the following problem and find no solution:
    I would like to use the following varibable possibility:

    from:
    /pl/whatever/[ASIN]/
    to:
    https://www.amazon.de/gp/product/%5BASIN%5D/?tag=my_amazon_tag_xx

    So completely without map – completely variable.
    The link structure is the same for all articles – the URL always begins with:
    Domain /pl/whatever /then comes the ASIN – should then forward to:
    https://www.amazon.de/gp/product/ASIN/?tag=my_amazon_tag_xx

    whereby the ASIN should always be taken over from the outgoing URL.

    Is this construction possible?

    It would be nice to hear from you!

    THANKS

  6. Need a little help too….

    I want to make urls like:
    http://www.sitename.com/page.html
    http://sitename.com/page2.html.

    Be redirected to be:
    https://www.sitename.com/page.html
    https://www.sitename.com/page2.html

    At the moment the server takes those 2 urls and makes them:
    https://www.sitename.com

    I’ve been told that this rule would work:
    return 301 https://www.discountcoffee.com$request_uri;

    At one point an admin told us to use this rule:
    rewrite ^ https://www.discountcoffee.com$request_uri? permanent;

    But that rule crashed the website so we removed it.

    Any help is greatly appreciated!

  7. Hi,

    Should:

    “`
    if ( $redirect_uri ) {
    return 301 $redirect_uri;
    }
    “`

    read

    “`
    if ( $request_uri ) {
    return 301 $redirect_uri;
    }
    “`

    1. No. We’re using the value from the map, which only will be truthy if a redirect is to be made.

  8. Hello Guys I have an issue , have a nginx reverse proxy , need to write a 301 redirect a url with pdf to another url with pdf

    ex: https://www.xyz.com/folder1/folder2/qwer.pdf to https://www.abc.com/folder1/folder2/asdf.pdf

    used the below but no use ..

    ************************************************************

    location /folder1/folder2/qwer.pdf {

    return 301 https://www.abc.com/folder1/folder2/asdf.pdf;
    }
    **************************************************************

    Any idea on this ? Please help

  9. Is there any way to do a redirect in a way that is reminiscent of a 301 with .htaccess? I don’t have direct access to my server as I’m using a friends server and therefore need to be able to do this (preferably) though FTP.

    Thanks!

    1. AFAIK Nginx doesn’t have an option to enable looking for an additional server config files in every directory it hits and loading them dynamically, like Apache has with the default name of the additional config file being .htaccess.

      You can ask your friend to configure a script (e.g. a PHP file) as a 404 handler for your site, then setup the redirects in that script.

  10. We are having problems with subdomains being redirected to the main domain. We’re using a redirect map, as you describe. Could that be the cause? Is “return 301 $redirect_uri” redirecting the subdomains? If so, how can we keep the map, but correct this behavior?

    We can’t find anything else that might be causing this.

    Thanks for any help!

  11. Nice article! Thank you and congratulations!

    I’m not an expert in NGINX and am facing some issues in the following scenario: I have a POST request coming as http://old_app_name/rest/mymethod that I need to convert to https://new_app_name/rest/mymethod, preserving the request body. The application itself is configured as an upstream. If I use a proxy_pass rule directly, it won’t work because the URL, internally, is going to be invalid. I’d need to first modify it (replace ‘old_app_name’ by ‘new_app_name’) and only then do move forward. I already tried rewrite rules, return with 307, proxy_pass, and nothing worked. What, based on your experience, would you recommend in this scenario?

    Thanks so much, in advance.

    Cheers,
    Pedro

    1. Hi Pedro, and thank you for your comment.

      As you mention, using a HTTP 307 response code (or even 308 Permanent Redirect) is a technically correct solution. But you have to remember to return a Location header as well, so the client knows where to direct the POST request. I am not sure how many of your clients will understand a 307 (or the newer 308) response code.

      The failsafe approach would be to use a proxy_pass, which should work just fine. Use proxy_set_header Host $host; if you want to pass on the Host header from the client, or set your own with proxy_set_header Host example.com;. It might be a good idea to log all proxied requests and ask those clients to update their configurations to use the new URL.

      1. I’m very close to getting this to work… Unfortunately the external is http & internal is https… And there’s a whole bunch of complexity with this letsencrypt-nginx-companion docker setup… Currently only working with SSL (and the link I’m trying to change is http://)

  12. thanks for sharing
    This worked great on a wordpress multisite redirect for non www. prefixed URLS to redirect to the www urls which can truly mess up SEO ranking if it’s not in place

    Redirect non-www to www:
    if ( $host !~ ^www\. ) {
    return 301 $scheme://www.$host$request_uri;
    }

  13. Do you know of a way to have the response body customized when using the map construct like

    server {
    […]
    if ( $redirect_uri ) {
    return 302 $redirect_uri;
    }
    }
    ?

    This method has the intended effect of returning HTTP 302 response code with mapped URL, but the smaller unintended effect of returning only this stock 302 body:

    302 Found

    302 Found
    nginx

    I’ve found no way to apply error_page directives, which I have used elsewhere in the site. the if {} block does not allow a root directive either.

  14. if ( $host ~ ^www\.(?.+)$ ) {
    return 301 $scheme://$domain$request_uri;
    }
    Worked perfectly, thanks for sharing!

  15. Hi i need help…
    i have my site domain.com in one server using nginx.
    Everytime that someone hit domain.com/~username i need that show http://domain2.com/~username/ that reside in other server.
    But maintaining the original first domain name domain.com/~username

  16. I have a question.

    I have a backend.conf file which includes 2 server blocks and each of which has some locations. When a user sends a request to a specific URL, Nginx serves it and there is no problem. But when the backend or a specific location is not accessible due to maintenance or other issues, the user faces a 502 message or other error pages. I need to put a virtual server which serves a fixed JSON file to be served automatically when another server blocks are not accessible. What can I do?

Comments are closed.