How to Use Variables in Nginx Configuration

How to Use Variables in Nginx Configuration

How to Use Variables in Nginx Configuration

If you’ve spent any real time editing an nginx.conf file, you’ve probably noticed those little tokens scattered around that start with a dollar sign — things like $host, $remote_addr, or $request_uri. Those are Nginx variables, and once you understand how they work, you’ll start writing configurations that are dramatically more flexible than the static, hardcoded blocks most tutorials show you.

I want to walk you through what Nginx variables actually are, how they’re evaluated, how to create your own, and where they trip people up. By the end of this guide you’ll be comfortable using variables for logging, routing, access control, and rewriting — and you’ll understand why Nginx variables don’t behave quite like variables in a normal programming language.

What Are Nginx Variables, Really?

Nginx variables are named placeholders that get evaluated at request time. Unlike a variable in Python or Bash, an Nginx variable isn’t “set once and read later” in the traditional sense — it’s more like a lazy-evaluated expression tied to the lifecycle of a single HTTP request. Every time a request comes in, Nginx computes the value of a variable the first time it’s referenced, caches that value for the rest of the request, and then throws it away once the request finishes.

This matters because it explains a lot of confusing behavior beginners run into. For example, if you try to use if blocks combined with variables in the wrong context, or if you expect a variable to persist across requests like a global counter, you’ll be disappointed. Nginx variables live and die with the request.

There are two broad categories:

  1. Built-in variables — provided by Nginx core and its modules (like ngx_http_core_module, ngx_http_rewrite_module, ngx_http_ssl_module, etc.). Examples include $uri, $args, $http_user_agent, $scheme, $ssl_protocol.
  2. User-defined variables — variables you create yourself using the set directive or as a side effect of directives like rewrite, map, or geo.

Why Bother With Variables At All?

You might be wondering why you’d need variables when you could just hardcode values into your location blocks. The honest answer is: for simple sites, you often don’t need them. But variables become essential the moment you want your configuration to adapt based on the incoming request — things like:

Once your config needs to “think” a little, variables are how you give it that ability.

Requirements

Before you start experimenting, make sure you have:

nginx -v
sudo nginx -t && sudo systemctl reload nginx

I always run nginx -t before reloading. It validates syntax without taking the server down, and it has saved me from outages more times than I’d like to admit.

Built-in Variables You’ll Use Constantly

Here are the variables I reach for most often in real configurations:

A subtle but important distinction: $uri vs $request_uri. $request_uri never changes during request processing — it’s the raw, original URI. $uri can change as Nginx performs internal rewrites (for example, via rewrite or try_files). If you’re debugging weird rewrite behavior, logging both side by side will save you a lot of head-scratching.

Creating Your Own Variables with set

The set directive, part of the rewrite module, lets you define a custom variable:

server {
    listen 80;
    server_name example.com;

    set $my_custom_var "hello";

    location / {
        add_header X-Custom-Header $my_custom_var;
        return 200 "Value is: $my_custom_var\n";
    }
}

A few rules to keep in mind:

Combining Variables and Conditionals

This is where variables start to earn their keep. A common pattern is detecting something about the request and adjusting behavior:

server {
    listen 80;
    server_name example.com;

    set $mobile_redirect "no";

    if ($http_user_agent ~* "(android|iphone|mobile)") {
        set $mobile_redirect "yes";
    }

    location / {
        if ($mobile_redirect = "yes") {
            return 302 https://m.example.com$request_uri;
        }
        root /var/www/example.com;
        index index.html;
    }
}

I want to be upfront about something: Nginx’s documentation itself warns that if is “evil” in certain contexts, particularly when used inside location blocks for anything beyond return or rewrite. It doesn’t behave like a conditional in a general-purpose language — it can produce surprising results when combined with other directives in the same block. For anything beyond a simple check, the map directive (covered next) is the safer, more idiomatic tool.

The map Directive: The Right Way to Do Conditional Variables

map creates a new variable whose value depends on another variable, using a lookup table. It’s evaluated once per request and doesn’t suffer from the same pitfalls as if.

http {
    map $http_user_agent $is_bot {
        default 0;
        "~*bot|crawler|spider" 1;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            if ($is_bot) {
                return 403;
            }
            root /var/www/example.com;
        }
    }
}

Note that map must live in the http context, not inside server or location. This trips up a lot of people who try to nest it directly inside a virtual host block.

Another practical example — routing based on a custom header for canary deployments:

http {
    map $http_x_canary $backend_pool {
        default   "stable_backend";
        "true"    "canary_backend";
    }

    upstream stable_backend {
        server 10.0.0.10:8080;
    }

    upstream canary_backend {
        server 10.0.0.20:8080;
    }

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

        location / {
            proxy_pass http://$backend_pool;
        }
    }
}

This lets you send a specific subset of traffic (anyone sending X-Canary: true) to a different backend pool entirely, without duplicating your whole server block.

Using Variables in Logging

Custom log formats are one of the most practical uses of variables. Here’s a log format that captures more than the default combined format:

http {
    log_format detailed '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         'rt=$request_time uct="$upstream_connect_time" '
                         'uht="$upstream_header_time" urt="$upstream_response_time"';

    server {
        access_log /var/log/nginx/access.log detailed;
        ...
    }
}

The upstream timing variables ($upstream_response_time, $upstream_connect_time, $upstream_header_time) are gold when you’re trying to figure out whether slowness is coming from Nginx itself or from your backend application.

Complete Example Configuration

Here’s a fuller example tying several of these ideas together — a config that logs detailed timing, redirects mobile users, and routes canary traffic:

http {
    log_format detailed '$remote_addr - [$time_local] "$request" '
                         '$status $body_bytes_sent rt=$request_time '
                         'ua="$http_user_agent"';

    map $http_user_agent $is_mobile {
        default 0;
        "~*android|iphone|ipad|mobile" 1;
    }

    map $http_x_canary $backend_pool {
        default   "stable_backend";
        "true"    "canary_backend";
    }

    upstream stable_backend {
        server 10.0.0.10:8080;
    }

    upstream canary_backend {
        server 10.0.0.20:8080;
    }

    server {
        listen 80;
        server_name example.com;

        access_log /var/log/nginx/example.access.log detailed;

        location / {
            if ($is_mobile) {
                return 302 https://m.example.com$request_uri;
            }

            proxy_pass http://$backend_pool;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Testing Your Configuration

Always validate syntax before reloading:

sudo nginx -t

To actually verify variable behavior, I like using curl with custom headers to simulate different clients:

# Simulate a mobile user agent
curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)" -I http://example.com/

# Simulate canary header
curl -H "X-Canary: true" -I http://app.example.com/

You can also temporarily add a debug endpoint that echoes variable values back, which is invaluable while you’re still building out logic:

location /debug {
    default_type text/plain;
    return 200 "host=$host uri=$uri args=$args is_mobile=$is_mobile\n";
}

Just remember to remove or restrict access to that endpoint before going to production — you don’t want to leak internal routing logic or headers to the public internet.

Troubleshooting Common Issues

“unknown directive” or “invalid variable name” errors on reload. This almost always means you’re referencing a variable that doesn’t exist yet, or you have a typo. Nginx variable names are case-sensitive and must be all lowercase with underscores by convention.

A variable seems “stuck” at its default value. Check whether you’re evaluating it before it’s set. Directive order inside a block doesn’t matter for set, but map blocks must be declared in http context before the server block uses them, or Nginx will fail to start.

if behaves unpredictably when mixed with other directives. As mentioned earlier, avoid combining if with things like proxy_pass selection logic in complex ways. If you find yourself nesting multiple if blocks, refactor into map.

Custom variables not appearing in logs. Double check your log_format directive is actually referenced by the access_log directive in the relevant server block — defining a format doesn’t automatically apply it.

Security Considerations

Variables derived directly from client input — like $http_user_agent, $http_referer, or any custom header — should never be trusted blindly. If you’re using them in proxy_pass targets, return statements, or anywhere that could be interpreted as a path or command, sanitize or validate them first. A classic mistake is doing something like proxy_pass $http_x_backend; without validating that header, which opens the door to request smuggling or SSRF-style abuse if an attacker controls that header.

Similarly, be cautious with logging raw user input into log files. Header injection via newline characters has historically been an issue in poorly configured web servers, so keep Nginx updated and avoid manually concatenating unsanitized variables into shell commands if you’re scripting around your logs.

Performance Tips

Variables themselves are cheap — Nginx’s variable system is designed to be fast, with lazy evaluation meaning a variable’s value is only computed if something actually references it during that request. That said:

Real-World Use Cases

Best Practices

Once variables click, you start seeing Nginx less as a static file server and more as a lightweight request-processing engine. That shift in mindset is genuinely useful, whether you’re just serving a personal blog or running edge routing logic for a fleet of microservices.

Exit mobile version