88

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?

tomka
  • 1,275
  • 1
  • 11
  • 17

4 Answers4

107

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
Danack
  • 24,939
  • 16
  • 90
  • 122
  • Alright, thanks for you answer and for sharing your solution to that problem. – tomka Mar 15 '13 at 18:24
  • Thanks for the reply. Any particular reason why the developers don't allow those variables? – Nicolas Mattia Apr 20 '15 at 15:59
  • 1
    http://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." – Danack Apr 20 '15 at 16:01
  • That was super quick, thank you. I added part of your comment to your answer. – Nicolas Mattia Apr 20 '15 at 16:08
  • Template engine is the silver bullet for all the config files which require programming language level of flexibility. – Mingjiang Shi Sep 10 '15 at 03:59
  • 5
    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) – Ricky Boyce Feb 25 '16 at 22:59
  • 2
    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. – Ludwig Jul 06 '16 at 19:10
  • @Ludwig You mean it would be easy? https://signalvnoise.com/posts/439-four-letter-words But anyway - but separating it out and not having that feature, it means that the tools to do this don't need to be maintained by the nginx guys, and also that those tools can evolve separately. – Danack Jul 07 '16 at 08:42
  • @Danack I was thinking about a module. A module could but does not have to be developed and maintained by the xnginx core developers. (btw where do you read 'easy'?) – Ludwig Jul 08 '16 at 09:52
31

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.

Varstahl
  • 575
  • 4
  • 14
  • 1
    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. – TastyWheat Aug 21 '20 at 14:59
  • 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 – codejedi365 Feb 07 '21 at 17:56
  • Strangely doesn't seem to work in nginx/1.20.1 – sivann Jan 13 '23 at 07:40
7

You could do the opposite of what you proposed.

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

location (/test_/something {
   set $folder $1;
}
rstackhouse
  • 2,238
  • 24
  • 28
  • 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. – rstackhouse May 02 '14 at 19:15
  • 2
    because it's fun to +1 other people. – Flavius Nov 01 '18 at 15:45
4

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)
Ricky Boyce
  • 1,772
  • 21
  • 26