Sooner or later, every server admin needs a redirect. You installed an SSL certificate and want every visitor on HTTPS. You moved your site to a new domain and do not want to lose your search ranking. You restructured your URLs and old links now point to pages that no longer exist.
Nginx handles all of these cases with a few lines of configuration, but the details matter. Choosing the wrong redirect code can hurt your SEO, forgetting a single variable can drop the visitor on your homepage instead of the page they asked for, and a careless rule can send browsers into an infinite redirect loop.
This tutorial shows you how to create Nginx redirect configurations the right way. You will learn the difference between 301 and 302 redirects, when to use return and when to use rewrite, and how to test everything with curl before your users ever see it. It is written for beginner to intermediate sysadmins and developers running Nginx on Ubuntu, but the configuration works the same on any Linux distribution.
How an Nginx Redirect Works
A redirect is just an HTTP response. Instead of returning your page content, Nginx returns a status code in the 3XX range plus a Location header that tells the browser where to go instead. The browser then makes a second request to that new URL automatically. The visitor usually never notices, the address bar simply changes.
Two status codes cover almost every situation:
- 301 Moved Permanently: the resource has moved for good. Browsers cache this aggressively, and search engines transfer the ranking of the old URL to the new one. Use it for HTTP to HTTPS, www changes, and domain migrations.
- 302 Found (temporary): the resource is somewhere else for now, but the original URL is still the real one. Browsers do not cache it by default, and search engines keep indexing the old URL. Use it for maintenance pages, A/B tests, or anything you plan to undo.
The practical rule: if you are not sure the change is forever, start with a 302. You can always upgrade it to a 301 later. Doing it the other way around is painful because browsers keep serving the cached 301 long after you removed it from your config.
There are two more codes worth knowing: 308 is the permanent redirect that also preserves the request method (a POST stays a POST), and 307 is its temporary equivalent. Classic 301/302 redirects may turn a POST into a GET. For a normal website this rarely matters, but if you redirect API traffic, use 308/307 instead.
return vs rewrite
Nginx gives you two directives for redirecting:
returnimmediately stops processing and sends the response. It is fast, predictable, and the recommended option whenever the target URL is fixed or can be built from simple variables.rewritematches the URI against a regular expression and can transform it. It is more powerful, but every request pays the regex cost and complex rules become hard to reason about.
The official Nginx documentation recommends return for plain redirects and rewrite only when you genuinely need pattern matching. We will use both in this tutorial, each where it fits.
Prerequisites
Before you start, make sure you have:
- A server with Nginx installed (any recent version works,
sudo apt install nginxon Ubuntu is enough) - A user with
sudoprivileges - Basic familiarity with Nginx configuration files in
/etc/nginx/sites-available/or/etc/nginx/conf.d/ curlinstalled for testing (sudo apt install curl)
It also helps to understand how Nginx picks which server block handles a request, because redirects usually live in their own server block. If you are not sure about that part, read Understanding Nginx Server Block first.
All examples use example.com. Replace it with your real domain.
Step 1: Redirect HTTP to HTTPS
This is the most common redirect on the internet. After you install an SSL certificate, you want every plain HTTP request pushed to the encrypted version of your site.
Open your site configuration:
sudo nano /etc/nginx/sites-available/example.com
Use two server blocks: one that only redirects, and one that actually serves the site:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/example.com;
index index.html;
}
The first block listens on port 80 and does nothing except answer every request with a 301. The important detail is $request_uri. This variable contains the full original path plus the query string, so a request to http://example.com/blog/post?page=2 lands on https://example.com/blog/post?page=2. If you forget it and write return 301 https://example.com;, every visitor gets dumped on your homepage and every deep link on the internet to your site breaks.
Test the syntax and reload:
sudo nginx -t
sudo systemctl reload nginx
nginx -t checks the configuration without touching the running server, and reload applies it without dropping active connections. Get into the habit of running both after every change in this tutorial.
If you have not set up the SSL certificate yet, do that first: Secure Nginx with Let’s Encrypt SSL Using Certbot on Ubuntu walks through it, and Certbot can even generate this exact redirect for you.
Step 2: Redirect www to Non-www (or the Other Way Around)
Search engines treat www.example.com and example.com as two different websites. If both serve content, you split your ranking between them. Pick one canonical version and redirect the other.
To redirect www to the root domain, add a dedicated server block:
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;
}
Note that this block still needs the SSL certificate. A browser visiting https://www.example.com performs the TLS handshake before Nginx can send the redirect, so the certificate must cover the www name (Let’s Encrypt certificates usually include both when you request them with -d example.com -d www.example.com).
If you prefer the www version as canonical, just swap the names:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://www.example.com$request_uri;
}
Combined with Step 1, a visitor typing http://www.example.com/about now hops twice: first to https://example.com/about (or you can point the port 80 block straight at the final version to save one hop, which is exactly what the Step 1 config already does).
Step 3: Redirect an Old Domain to a New Domain
When you rebrand or consolidate websites, you want the old domain to forward all its traffic, and its accumulated SEO value, to the new one.
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;
}
This block answers on both HTTP and HTTPS for the old domain and forwards everything, path and query string included, to the new domain with a 301. Keep the old domain registered and this configuration running for at least several months (a year is safer), because search engines and old backlinks take time to catch up.
One thing people forget: the old domain still needs a valid SSL certificate for as long as the redirect runs. HTTPS visitors reach the redirect only after a successful TLS handshake, and an expired certificate shows a scary browser warning instead of your new site.
Step 4: Redirect a Single Page
Not every redirect is site-wide. When you rename a page or move a post, redirect the exact URI using a location block with an exact match:
server {
listen 443 ssl;
server_name example.com;
...
location = /old-page {
return 301 /new-page;
}
}
The = modifier matches only the exact URI /old-page, nothing else. Also notice the target has no scheme or domain: when you return a path starting with /, Nginx automatically expands it to the same scheme and host, which keeps the rule working even if you later change domains.
You can stack as many of these as you need. If you have dozens of them, a map keeps things tidy:
map $request_uri $redirect_target {
/old-page /new-page;
/old-blog /blog;
/2021/pricing /pricing;
}
server {
listen 443 ssl;
server_name example.com;
...
if ($redirect_target) {
return 301 $redirect_target;
}
}
The map block goes in the http context (outside your server block, for example in /etc/nginx/conf.d/redirect-map.conf). Lookups against a map are fast even with hundreds of entries, much faster than a long chain of regex location blocks.
How Nginx chooses between competing location blocks is a topic of its own, and worth understanding before you add many of them: Understanding Nginx Location Block covers the matching rules with examples.
Step 5: Pattern Redirects with rewrite
When the new URL must be built from pieces of the old one, return is not enough and rewrite earns its place. Say you moved your blog from /blog/2024/my-post to /posts/my-post, so you need to capture the post slug:
server {
listen 443 ssl;
server_name example.com;
...
rewrite ^/blog/\d{4}/(.*)$ /posts/$1 permanent;
}
Reading the rule left to right: the regex ^/blog/\d{4}/(.*)$ matches any URI that starts with /blog/, followed by a four-digit year, and captures everything after it into $1. The replacement /posts/$1 builds the new URI, and the permanent flag makes it a 301 (use redirect instead for a 302).
Another everyday example, stripping a .html suffix from legacy URLs:
rewrite ^(/.*)\.html$ $1 permanent;
A request to /about.html becomes a 301 to /about.
Two warnings about rewrite. First, every request evaluates the regex, so keep the pattern list short and put the most common patterns first. Second, if the replacement string starts with http:// or https://, Nginx redirects immediately, but if it is a plain path and you omit the flag, Nginx rewrites the URI internally and processes it again without telling the browser, which is a different behavior entirely. For redirects, always include permanent or redirect.
Step 6: Test Your Redirects with curl
Never trust a redirect just because the config reloaded cleanly. Browsers cache redirects, so testing in a browser can show you stale results. curl shows you exactly what Nginx sends:
curl -I http://example.com/blog/post?page=2
Expected output:
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/blog/post?page=2
Check two things: the status code is what you intended (301 vs 302), and the Location header contains the full original path and query string.
To follow the whole redirect chain to its end, add -L:
curl -IL http://www.example.com/about
HTTP/1.1 301 Moved Permanently
Location: https://example.com/about
HTTP/2 200
If you see three or more hops, consider collapsing them: each hop adds latency, and search engines dislike long chains. Every port 80 block should point directly at the final canonical URL, not at an intermediate one.
Common Mistakes and Troubleshooting
Redirect loop (ERR_TOO_MANY_REDIRECTS). The classic cause is a redirect whose target matches the same rule again, for example a port 443 block that redirects to HTTPS, or an HTTP-to-HTTPS redirect behind a load balancer or CDN that terminates TLS and talks plain HTTP to your backend. In the load balancer case, do not redirect based on the port, check the X-Forwarded-Proto header instead:
if ($http_x_forwarded_proto = "http") {
return 301 https://example.com$request_uri;
}
Redirect lands on the homepage instead of the requested page. You forgot $request_uri in the target. return 301 https://example.com; throws away the path; return 301 https://example.com$request_uri; keeps it.
Old redirect will not go away. Browsers cache 301 responses, sometimes for a very long time. Test with curl (which never caches) or a private browser window. This is also the reason to use 302 while you are still experimenting.
Query string duplicated. $request_uri already includes the query string. Writing return 301 https://example.com$request_uri$args; produces URLs like /page?x=1x=1. Use $request_uri alone, or $uri (path only, no query string) if you deliberately want to drop the arguments.
Changes have no effect. Run sudo nginx -t to catch syntax errors, then make sure you actually reloaded (sudo systemctl reload nginx). If it still misbehaves, another server block may be catching the request first, which brings you back to server block matching rules.
Best Practices
- Use
returnby default,rewriteonly for patterns. It is faster and much easier to read six months later. - Start with 302, commit with 301. Once browsers cache a wrong 301, you cannot take it back from your side.
- Always carry
$request_uri. Preserving the path and query string is the difference between a professional migration and thousands of broken deep links. - Keep redirect chains to one hop. Point every legacy URL directly at the final destination.
- Keep SSL certificates valid on redirecting domains. A redirect-only domain still terminates TLS before it can redirect.
- Test with
curl -ILafter every change, and re-test the old URLs a few weeks after a migration to confirm nothing regressed.
Conclusion
You now know how to build every redirect a typical website needs: HTTP to HTTPS, www to non-www, old domain to new domain, single-page moves with exact-match locations and maps, and pattern-based moves with rewrite. You also know the two rules that prevent most redirect disasters: choose 301 vs 302 deliberately, and never drop $request_uri.
If you want to keep improving your Nginx setup, a good next step is performance: Optimize Nginx with Gzip Compression and Browser Caching on Ubuntu covers compression and cache headers, and Configure Nginx Rate Limiting on Ubuntu shows how to protect the same server blocks you just configured from abusive clients.