94

In Nginx, I'm trying to define a variable which allows me to configure a sub-folder for all my location blocks. I did this:

set $folder '/test';

location $folder/ {
   [...]
}

location $folder/something {
   [...]
}

Unfortunately, this doesn't seem to work. While Nginx doesn't complain about the syntax, it returns a 404 when requesting /test/. If I write the folder in explicitly, it works. So how can I use variables in location blocks?

4 Answers 4

113

You can't. Nginx doesn't really support variables in config files, and its developers mock everyone who ask for this feature to be added:

"[Variables] are rather costly compared to plain static configuration. [A] macro expansion and "include" directives should be used [with] e.g. sed + make or any other common template mechanism." http://nginx.org/en/docs/faq/variables_in_config.html

You should either write or download a little tool that will allow you to generate config files from placeholder config files.

Update The code below still works, but I've wrapped it all up into a small PHP program/library called Configurator also on Packagist, which allows easy generation of nginx/php-fpm etc config files, from templates and various forms of config data.

e.g. my nginx source config file looks like this:

location  / {
    try_files $uri /routing.php?$args;
    fastcgi_pass   unix:%phpfpm.socket%/php-fpm-www.sock;
    include       %mysite.root.directory%/conf/fastcgi.conf;
}

And then I have a config file with the variables defined:

phpfpm.socket=/var/run/php-fpm.socket
mysite.root.directory=/home/mysite

And then I generate the actual config file using that. It looks like you're a Python guy, so a PHP based example may not help you, but for anyone else who does use PHP:

<?php

require_once('path.php');

$filesToGenerate = array(
    'conf/nginx.conf' => 'autogen/nginx.conf',
    'conf/mysite.nginx.conf' => 'autogen/mysite.nginx.conf',
    'conf/mysite.php-fpm.conf' => 'autogen/mysite.php-fpm.conf',
    'conf/my.cnf' => 'autogen/my.cnf',
);

$environment = 'amazonec2';

if ($argc >= 2){
    $environmentRequired = $argv[1];

    $allowedVars = array(
        'amazonec2',
        'macports',
    );

    if (in_array($environmentRequired, $allowedVars) == true){
        $environment = $environmentRequired;
    }
}
else{
    echo "Defaulting to [".$environment."] environment";
}

$config = getConfigForEnvironment($environment);

foreach($filesToGenerate as $inputFilename => $outputFilename){
    generateConfigFile(PATH_TO_ROOT.$inputFilename, PATH_TO_ROOT.$outputFilename, $config);
}


function    getConfigForEnvironment($environment){
    $config = parse_ini_file(PATH_TO_ROOT."conf/deployConfig.ini", TRUE);
    $configWithMarkers = array();
    foreach($config[$environment] as $key => $value){
        $configWithMarkers['%'.$key.'%'] = $value;
    }

    return  $configWithMarkers;
}


function    generateConfigFile($inputFilename, $outputFilename, $config){

    $lines = file($inputFilename);

    if($lines === FALSE){
        echo "Failed to read [".$inputFilename."] for reading.";
        exit(-1);
    }

    $fileHandle = fopen($outputFilename, "w");

    if($fileHandle === FALSE){
        echo "Failed to read [".$outputFilename."] for writing.";
        exit(-1);
    }

    $search = array_keys($config);
    $replace = array_values($config);

    foreach($lines as $line){
        $line = str_replace($search, $replace, $line);
        fwrite($fileHandle, $line);
    }

    fclose($fileHandle);
}

?>

And then deployConfig.ini looks something like:

[global]

;global variables go here.

[amazonec2]
nginx.log.directory = /var/log/nginx
nginx.root.directory = /usr/share/nginx
nginx.conf.directory = /etc/nginx
nginx.run.directory  = /var/run
nginx.user           = nginx

[macports]
nginx.log.directory = /opt/local/var/log/nginx
nginx.root.directory = /opt/local/share/nginx
nginx.conf.directory = /opt/local/etc/nginx
nginx.run.directory  = /opt/local/var/run
nginx.user           = _www
Sign up to request clarification or add additional context in comments.

11 Comments

Alright, thanks for you answer and for sharing your solution to that problem.
nginx.org/en/docs/faq/variables_in_config.html "Variables should not be used as template macros. Variables are evaluated in the run-time during the processing of each request, so they are rather costly compared to plain static configuration. Using variables to store static strings is also a bad idea. Instead, a macro expansion and "include" directives should be used to generate configs more easily and it can be done with the external tools, e.g. sed + make or any other common template mechanism."
Im sure it wouldn't be hard for nginx to compile static variables on startup, just like how they do with the includes (logical presumption)
Same thoughts as @RickyB had here. Why not having macro abilities with variables (like in Apache) that are replaced during start, restart or reload and kept as static configs in memory? So there would be no need of extra tools and workarounds.
some comments here conflate Template variables on Reload/Restart vs. those "evaluated at run-time ... during each request." The point is Nginx mass concurrency and performance. Rendering variables on each request other than Maps && certain things is the antithesis of Nginx core, and at least the developers are very clear on this ethic so they don't ruin the software.
|
35

This is many years late but since I found the solution I'll post it here. By using maps it is possible to do what was asked:

map $http_host $variable_name {
    hostnames;

    default       /ap/;
    example.com   /api/;
    *.example.org /whatever/;
}

server {
    location $variable_name/test {
        proxy_pass $auth_proxy;
    }
}

If you need to share the same endpoint across multiple servers, you can also reduce the cost by simply defaulting the value:

map "" $variable_name {
    default       /test/;
}

Map can be used to initialise a variable based on the content of a string and can be used inside http scope allowing variables to be global and sharable across servers.

5 Comments

The map module was definitely invaluable for my team to set variables dynamically (i.e. per request) but it was very messy because of restrictions where map and if can be used. If the variables are static I think a template is the better solution.
Great answer! this helped me set a variable that was reused multiple times inside of my http-serve.conf specific to that a single site
Strangely doesn't seem to work in nginx/1.20.1
I've spent hours debugging this, until I realized that once $variable_name is set, it will not update it's value again, even if $http_host changes (for the same request). I was able to find 0 documentation or references on this, so posting in case someone comes here from google
Also, what's that syntax inside the map where you write just hostnames; without a value?`
7

You could do the opposite of what you proposed.

location (/test)/ {
   set $folder $1;
}

location (/test_/something {
   set $folder $1;
}

2 Comments

I made the assumption that the author of the question was trying to tell Nginx what url his application was expecting. I was simply suggesting that rather than do that, Nginx could tell his application what url was used to access it. Matching ([^/]+) would be more useful than matching (/test), as in the example I gave, but the results would be the same.
because it's fun to +1 other people.
3

A modified python version of @danack's PHP generate script. It generates all files & folders that live inside of build/ to the parent directory, replacing all {{placeholder}} matches. You need to cd into build/ before running the script.

File structure

build/
-- (files/folders you want to generate)
-- build.py

sites-available/...
sites-enabled/...
nginx.conf
...

build.py

import os, re

# Configurations
target = os.path.join('.', '..')
variables = {
  'placeholder': 'your replacement here'
}


# Loop files
def loop(cb, subdir=''):
  dir = os.path.join('.', subdir);

  for name in os.listdir(dir):
    file = os.path.join(dir, name)
    newsubdir = os.path.join(subdir, name)

    if name == 'build.py': continue
    if os.path.isdir(file): loop(cb, newsubdir)
    else: cb(subdir, name)


# Update file
def replacer(subdir, name):
  dir  = os.path.join(target, subdir)
  file = os.path.join(dir, name)
  oldfile = os.path.join('.', subdir, name)

  with open(oldfile, "r") as fin:
    data = fin.read()

  for key, replacement in variables.iteritems():
    data = re.sub(r"{{\s*" + key + "\s*}}", replacement, data)

  if not os.path.exists(dir):
    os.makedirs(dir)

  with open(file, "w") as fout:
    fout.write(data)


# Start variable replacements.
loop(replacer)

2 Comments

If you are using python it is advisable to use a jinja template IMHO
This does not handle nested expansion properly

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.