78

I want to create a rule in nginx that does two things:

  1. Removes the "www." from the request URI
  2. Redirects to "https" if the request URI is "http"

There are plenty of examples of how to do each of those things individually, but I can't figure out a solution that does both correctly (i.e. doesn't create a redirect loop and handles all cases properly).

It needs to handle all of these cases:

1. http://www.example.com/path
2. https://www.example.com/path
3. http://example.com/path
4. https://example.com/path

These should all end up at https://example.com/path (#4) without looping. Any ideas?

Devin
  • 903
  • 1
  • 8
  • 8
  • 1
    I just redirected www.mydomain.com to mydomain.com at the DNS level and added a 301 for non-https to https in nginx. Seems like that should be fine ¯\\_(ツ)_/¯ – jonathanbell May 26 '17 at 23:02
  • 'Removes the "www." from the request URI' - why? This breaks your scalability. – symcbean May 04 '21 at 22:35

11 Answers11

124

The best way to accomplish this is using three server blocks: one to redirect http to https, one to redirect the https www-name to no-www, and one to actually handle requests. The reason for using extra server blocks instead of ifs is that server selection is performed using a hash table, and is very fast. Using a server-level if means the if is run for every request, which is wasteful. Also, capturing the requested uri in the rewrite is wasteful, as nginx already has this information in the $uri and $request_uri variables (without and with query string, respectively).

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

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name example.com;

    <locations for processing requests>
}
kolbyjack
  • 8,039
  • 2
  • 36
  • 29
  • 2
    Is the middle block necessary? Isn't the first block already rewriting from www to non-www? – pbreitenbach May 16 '11 at 20:39
  • 6
    The first block only handles http. The middle block is necessary to redirect https requests from https:// www.example.com/ to https:// example.com/. (Sorry for the extra spaces, I can't make it show the https otherwise) – kolbyjack May 17 '11 at 01:03
  • 2
    just a minor formatting note - if you want to avoid making a link, you can put comment text inside back-quotes \` , the one under tilde. It would show up like: `https://example.com/` – Cyclops Jun 09 '11 at 21:13
  • 10
    the second block also needs cert info. – ricka May 06 '16 at 01:20
  • Semicolon is missing after `server_name www.example.com` in second block! – Levani Aug 12 '16 at 18:59
  • 3
    Trying this answer, I ran into another problem. Thought I could 301 redirect from `www.sub.example.com` to `sub.example.com` and then only obtain an SSL certificate for `sub.example.com` Now I know that ssl cert check happens before the 301 redirect, so it can not work. More explanation here: http://serverfault.com/a/358625/144811 – Gruzzles Nov 24 '16 at 18:08
  • @Gruzzles this is also a issue with cloudflare. You'd have to pay the $10/mo certificate to be able to redirect and proxy 2 subdomains ( www + something ). At least I found no way to do it without paying for the cert. – Freedo Sep 24 '18 at 04:13
  • the second block is necessary. It handles the redirect from httpS://www.example.com, while the first block handles redirect from http://www.example.com. – Max Ivak Feb 13 '19 at 20:39
  • should it be different certificates in the second and third blocks? one cert for www.example.com and another for example.com? – Max Ivak Feb 13 '19 at 20:40
  • This worked for me only after`CloudFlare > SSL/TLS > Overview > Full (strict)` and `CloudFlare > SSL/TLS > Origin Server > Authenticated Origin Pulls` https://stackoverflow.com/questions/55606390/digital-ocean-node-js-nginx-and-cloudflare-400-badrequest-no-required-ssl-cer#comment107616053_55606390 – Ryan Mar 26 '20 at 22:13
  • Cloud flare was giving me grief by just having a single 301 (kept saying origin server didnt have a cert, but it did and it was created for both with and without subdomain). This solved it! – ParoX Dec 09 '21 at 02:43
  • Very helpful, saved my day. – millisami Aug 05 '22 at 07:43
12

This works for me:

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

server {
    listen              443 ssl;
    server_name         www.yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    return              301 https://yourdomain.com$request_uri;
}

server {
    listen              443 ssl;
    server_name         yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

    # do the proper handling of the request
}

Keep in mind that both yourdomain.com and www.yourdomain.com must be in your SSL certificate. This is possible with a wildcard certificate or with a Server Alternate Name as explained here. Check https://www.startssl.com for nice and free certificates that do this. (Edith: beginning with Chrome version 56, startssl certificates will not be trusted anymore. Try https://letsencrypt.org/ instead.)

e18r
  • 221
  • 3
  • 6
  • This one actually works, but I thought it could be done in more clear way without a lot of duplicate config lines. – zloynemec Sep 18 '17 at 08:48
  • @zloynemec You could put the SSL stuff in a separate .conf file and use the `include` rule to add it to both SSL server blocks. – Person Apr 02 '18 at 09:03
  • Also if you are using cloudflare you need to pay the $10/mo cert to be able to redirect and proxy the 2 subdomains ( www + something ). Let me know if there is a workaround. – Freedo Sep 24 '18 at 04:17
9

After spending so much time with hundreds of similar cases, I've come up with the following snippet. It's short and can be easily tweaked to fit anything.

server {
    listen 80;
    listen 443 ssl;
    server_name example.com www.example.com;
    ssl_certificate /path/to/my/certs/example.com/fullchain.pem;
    ssl_certificate_key /path/to/my/certs/example.com/privkey.pem;

    # Redirect to the correct place, if needed
    set $https_redirect 0;
    if ($server_port = 80) { set $https_redirect 1; }
    if ($host ~ '^www\.') { set $https_redirect 1; }
    if ($https_redirect = 1) {
        return 301 https://example.com$request_uri;
    }

    location / {
    # ...
}

Oh but if is evil!

Yes it can be. But it exists for a reason, and should do no harm to those who know how to use it properly. ;)

emyller
  • 191
  • 1
  • 2
  • I like this, but do you have any data on the performance hit? Thank you! – Freedo Sep 24 '18 at 04:17
  • 1
    Honestly I never benchmarked that, but I believe there would be hardly an impact compared to separate rules since the effect is pretty much the same. – emyller Sep 24 '18 at 20:38
  • benchmark on redirection? it's not realy pertinent no? (true question, not a troll ^^) – Matrix Nov 23 '18 at 04:42
  • @Freedo Using three server blocks is the fastest in terms of performance; I tried to give the complete explanation [here](https://stackoverflow.com/a/72615689/7121513). – Ivan Shatsky Jun 15 '22 at 09:50
  • Years later, I see there is a performance difference and it could impact servers that handle a large amount of traffic. Although I'm curious to see numbers. BTW I see some good tips that could maybe minimize this difference here: https://www.getpagespeed.com/server-setup/nginx-locations-performance-impact-and-optimizations – emyller Jun 15 '22 at 14:28
3

I prefer to return with a response code so the browser knows you are redirecting it to another URL.

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

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

then another server configurations block for the https

server {
        listen   443 ssl;
        server_name  example.com;
        ...
    }
Scott Pack
  • 14,907
  • 10
  • 53
  • 83
montss
  • 406
  • 5
  • 11
1

If you have many domains and you are looking for a more generic approach without loosing performance and without listing all domains all the time, check this.

How it works?

  1. listen to 80 and redirect all http to https - including http://www. which will go to https://www.
  2. listen to 433, but only on www server names and redirect to non-www using regular expression
  3. listen to 433 for each of your non-www server name - this is where all the traffic will end up

Is it fast?
Yes! Even though we use RegExp, it's only in the www-versions block which returns 301. So all normal traffic will be handled without any additional processing cost.

  # Redirect everything to HTTPS (including "www")
  server {
    listen 80 default_server;
    listen [::]:80 default_server;
    return 301 https://$host$request_uri;
  }
  
  # Redirect away from "www" versions:
  server {
    listen [::]:443 ssl http2;
    listen 443 ssl http2;
    server_name www.example-1.com
                www.example-2.com
                www.example-3.com;
    # using generic "www" removal      // https://stackoverflow.com/questions/11323735/nginx-remove-www-and-respond-to-both/45676731#45676731
    if ( $host ~ ^www\.(.+)$ ) {
      set $without_www $1;
      rewrite ^ $scheme://$without_www$uri permanent;
    }
    # SSL settings:
    ssl_certificate /etc/letsencrypt/live/example/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example/privkey.pem; # managed by Certbot
  }
  
  # And finally one "server" block for each of your domains
  server {
    listen [::]:443 ssl http2;
    listen 443 ssl http2;
    server_name example-1.com;
    # SSL settings, etc...
  }
  server {
    server_name example-2.com;
    # ... listen, ssl, etc...
  }
  server {
    server_name example-3.com;
    # ... etc
  }

PS: if you need help with SSL setup, checkout Mozilla SSL Configuration Generator:
https://ssl-config.mozilla.org/

icl7126
  • 171
  • 5
0
server {
        listen 80;
        listen 443 ssl;
        server_name devly.co www.devly.co;

        ssl on;
        ssl_certificate /var/www/devly.co/cert/ssl-bundle.crt;
        ssl_certificate_key /var/www/devly.co/cert/devly_co.key;



        access_log   /var/log/nginx/devly.co.access.log rt_cache;
        error_log    /var/log/nginx/devly.co.error.log;
        root /var/www/devly.co/htdocs;
        index index.php index.htm index.html;

# force https-redirects
    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
}


}
AlShar
  • 1
0

This, works for me

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

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

    #location

}

p.s. with other solutions I got: ERR_TOO_MANY_REDIRECTS

  • This does not provide an answer to the question. Once you have sufficient [reputation](https://serverfault.com/help/whats-reputation) you will be able to [comment on any post](https://serverfault.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/501725) – djdomi Nov 04 '21 at 17:48
0

First, redirect all http [80] requests to https permanently [301] from the first server block. Configure SSL in the second server block with the same domain and subdomain.

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

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    root /var/www/html/app;

    server_name domain.com www.domain.com;

    ssl_certificate /etc/ssl/chain.crt;
    ssl_certificate_key /etc/ssl/private.key;
}
Robot Boy
  • 101
  • This doesn’t answer the “remove www” part of the question. This only redirects from clear text http to https – HBruijn Jul 25 '23 at 06:49
0

how about creating a server block for this purpose:

server{
    listen 80;
    server_name www.example.net example.net;
    rewrite ^(.*) https://example.net$1 permanent;
}

then restarting nginx

anthonysomerset
  • 4,233
  • 2
  • 21
  • 24
  • I get a "conflicting server name" error when restarting. Also, that command won't listen on port 443 for SSL and I need to worry about redirecting `https://www.example.com` to `https://example.com` as well. – Devin Apr 11 '11 at 16:56
0

I think this should work.

On your plain HTTP server definition something like anthonysomerset suggested, that is:

rewrite ^(.*) https://example.net$1 permanent;

Then on your SSL server definition:

if ($host ~ /^www\./) {
  rewrite ^(.*) https://example.net$1 permanent;
}

This way the redirect should only happen once per request no matter which URL the user goes to originally.

Eduardo Ivanec
  • 14,881
  • 1
  • 37
  • 43
  • That worked, thanks. I had to change your conditional to `if ($host = 'www.example.com') {` since your regex wasn't working for me, though. No idea why, as it looks correct. – Devin Apr 11 '11 at 18:20
  • Do note that [if is evil](http://wiki.nginx.org/IfIsEvil) and it's generally better to use a declarative way. – Blaise Mar 21 '15 at 06:35
0

Here's the full example that ended up working for me. The problem was that I didn't have the ssl details (ssl_certificate, etc.) in the www redirect block. Remember to check your logs (sudo tail -f /var/log/nginx/error.log)!

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80 default_server ipv6only=on;
    return 301 https://$host$request_uri;
}

# HTTPS — redirects www to non-www
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;
    return 301 https://example.com$request_uri;
}

# HTTPS — proxy all requests to the app (port 3001)
server {
    # Enable HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com sub.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;

    # For LetsEncrypt:
    location ~ /.well-known {
        root /var/www/html;
        allow all;
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:3001;
        proxy_ssl_session_reuse off;
        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
}