Understanding the Nginx location directive and redirects

The Nginx location directive chooses configuration based on the request URI. You can use it to serve files, proxy requests, or return redirects.

A basic redirect

For a small example of using a location block to serve files as plain text, see the NGINX text response guide.

This server redirects one exact path:

server {
  listen 80;
  server_name staging.example.com;
  root /var/www/staging.example.com/public_html;
 
  location = /old-page {
    return 308 https://www.example.com/new-page;
  }
}

308 is a permanent redirect that preserves the request method and body. Use 302 or 307 while testing if you do not want clients to cache the redirect.

Location modifiers

The syntax is:

location [ = | ~ | ~* | ^~ ] uri { ... }
ModifierMatch
No modifierURI prefix
=Exact URI
~Case-sensitive regular expression
~*Case-insensitive regular expression
^~URI prefix; skip regular expressions if this one wins

Nginx matches against the normalized URI path, without the query string.

How Nginx picks a location

For non-nested locations, the search is:

  1. An exact = match wins immediately.
  2. Nginx remembers the longest matching prefix.
  3. If that prefix uses ^~, it wins immediately.
  4. Otherwise, Nginx checks regular expressions in file order. The first match wins.
  5. If no regular expression matches, the remembered prefix wins.

Prefix order does not matter. Regular expression order does.

Prefix matches

A location without a modifier matches from the start of the URI:

location / {
  # Matches every URI
}
 
location /foo {
  # Matches /foo, /foo/bar, and /foobar
  # Does not match /bar/foo
}

Use a trailing slash when you mean a path segment:

location /foo/ {
  # Matches /foo/bar, but not /foobar
}

Prefix matching is case-sensitive on Linux, so /foo does not match /FOO. Nginx ignores ASCII case for prefix locations on macOS and Cygwin.

Exact matches

Use = when only one URI should match:

location = / {
  # Matches only /
}
 
location = /foo {
  # Matches only /foo
}

Regular expression matches

Nginx uses PCRE-compatible regular expressions. ~ is case-sensitive; ~* is case-insensitive.

This redirects an old tag URL and keeps its query string:

location ~ ^/tag/(?<slug>[^/]+)$ {
  return 308 /tags/$slug$is_args$args;
}

For /tag/nginx?source=old:

  • ^ anchors the match to the start of the URI
  • (?<slug>[^/]+) captures one path segment as $slug
  • $ anchors the match to the end
  • $is_args becomes ? when arguments exist, or an empty string otherwise
  • $args contains the query string

The result is /tags/nginx?source=old without needing an if block. See the complete list of Nginx variables.

More regular expression examples

Cache versioned static files

Long cache lifetimes work best for files with content hashes in their names:

location ~* \.(?:avif|css|gif|ico|jpe?g|js|mjs|otf|png|svg|ttf|webp|woff2?)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
  access_log off;
}

The escaped \. matches a literal dot. The ~* makes file extensions case-insensitive.

Keep favicon requests out of the logs

Put this before a broader regular expression location, because the first matching regular expression wins:

location ~* ^/(?:favicon[^/]*\.(?:ico|png|svg)|apple-touch-icon[^/]*\.png)$ {
  log_not_found off;
  access_log off;
}

Test redirects with curl

Browsers cache permanent redirects aggressively. Test with curl while tuning the configuration:

$ curl --silent --show-error --head \
    'http://staging.example.com/tag/nginx?source=old'
HTTP/1.1 308 Permanent Redirect
Server: nginx
Location: http://staging.example.com/tags/nginx?source=old

--head (-I) sends a HEAD request, so the server returns headers without a response body. It is fast and usually enough for testing Nginx redirects.

Some applications reject HEAD or handle it differently from GET. To test a normal GET, print its headers and discard its body:

curl --silent --show-error --dump-header - --output /dev/null \
  'http://staging.example.com/tag/nginx?source=old'

This still downloads the response body. It simply does not save or print it.

To print only the destination:

$ curl --silent --show-error --head \
    'http://staging.example.com/tag/nginx?source=old' \
  | grep -i '^location:'
Location: http://staging.example.com/tags/nginx?source=old

Add --location to follow the full redirect chain.

For more complex URI changes, see Nginx's rewrite directive.