164

[Cross-posted and edited down from https://stackoverflow.com/questions/21933955 as it was considered too sysadmin-like for StackOverflow.]

I have a docker container running Nginx, that links to another docker container. The host name and IP address of the second container is loaded into the Nginx container as environment variables on startup, but is not know before then (it's dynamic). I want my nginx.conf to use these values - e.g.

upstream gunicorn {
    server $APP_HOST_NAME:$APP_HOST_PORT;
}

How can I get environment variables into the Nginx configuration on startup?

EDIT 1

This is the entire file, after the suggested answer below:

env APP_WEB_1_PORT_5000_TCP_ADDR;
# Nginx host configuration for django_app

# Django app is served by Gunicorn, running under port 5000 (via Foreman)
upstream gunicorn {
    server $ENV{"APP_WEB_1_PORT_5000_TCP_ADDR"}:5000;
}

server {
    listen 80;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    location /static/ {
        alias /app/static/;
    }
    location /media/ {
        alias /app/media/;
    }
    location / {
        proxy_pass http://gunicorn;
    }
}

Reloading nginx then errors:

$ nginx -s reload
nginx: [emerg] unknown directive "env" in /etc/nginx/sites-enabled/default:1

EDIT 2: more details

Current environment variables

root@87ede56e0b11:/# env | grep APP_WEB_1
APP_WEB_1_NAME=/furious_turing/app_web_1
APP_WEB_1_PORT=tcp://172.17.0.63:5000
APP_WEB_1_PORT_5000_TCP=tcp://172.17.0.63:5000
APP_WEB_1_PORT_5000_TCP_PROTO=tcp
APP_WEB_1_PORT_5000_TCP_PORT=5000
APP_WEB_1_PORT_5000_TCP_ADDR=172.17.0.63

Root nginx.conf:

root@87ede56e0b11:/# head /etc/nginx/nginx.conf
user www-data;
worker_processes 4;
pid /var/run/nginx.pid;
env APP_WEB_1_PORT_5000_TCP_ADDR;

Site nginx configuration:

root@87ede56e0b11:/# head /etc/nginx/sites-available/default
# Django app is served by Gunicorn, running under port 5000 (via Foreman)
upstream gunicorn {
    server $ENV{"APP_WEB_1_PORT_5000_TCP_ADDR"}:5000;
}

server {
    listen 80;

Reload nginx configuration:

root@87ede56e0b11:/# nginx -s reload
nginx: [emerg] directive "server" is not terminated by ";" in /etc/nginx/sites-enabled/default:3
  • 8
    This isn't a generic solution for environment variables, but if you're wanting to use environment variables for the hostnames/IP addresses of upstream servers, note that Docker (at least in recent versions) modifies /etc/hosts for you. See docs.docker.com/userguide/dockerlinks This means, if your linked container is called 'app_web_1', docker will create a line in /etc/hosts in your Nginx container. So you can just replace server $ENV{"APP_WEB_1_PORT_5000_TCP_ADDR"}:5000; with server app_web_1:5000; – mozz100 Jul 10 '14 at 17:18
  • 1
    Thanks @mozz100 - that's incredibly useful - /etc/hosts entries are much more effective than env vars in this case. The only bit missing is what happens if the upstream container is restarted, and acquires a new IP. I am presuming that the child containers will still point to the original IP, not the new one? – Hugo Rodger-Brown Jul 11 '14 at 13:04
  • 1
    Yes, if you restarted app_web_1 it would get a new IP address, so you'd need to restart your nginx container too. Docker would restart it with an updated /etc/hosts so you wouldn't need to alter the nginx config file(s). – mozz100 Jul 14 '14 at 14:36

13 Answers 13

58

If you're using a link, docker sets up environment variables and adds an alias for the linked container in /etc/hosts. If you are able to hard code the port (or if it's just port 80) you can simply do:

upstream gunicorn {
    server linked-hostname:5000;
}

The port is only available in an environment variable, which cannot be used in the upstream module, nor in server or location blocks. They can only be referenced in the main config, which doesn't help you. You could do this with the openresty bundle that includes Lua.

If you don't want to use openresty/Lua, another option is to do some substitution at container startup. Your docker run command could create the link then run a wrapper script that performs the appropriate substitution:

#!/bin/bash
/usr/bin/sed -i "s/server<gunicorn_server_placeholder>/${APP_WEB_1_PORT_5000_TCP_ADDR}/" default
start nginx
  • Yup - current (working) solution is exactly this. Just seems a bit hacky. (That said, it's only a local dev environment, so hacking isn't a huge problem.) – Hugo Rodger-Brown Mar 18 '14 at 8:04
  • 4
    I ran into this, so I made a slightly more generic nginx container that automatically expands environment variables in the configuration. – Shepmaster Apr 26 '14 at 16:13
85

From the official Nginx docker file:

Using environment variables in nginx configuration:

Out-of-the-box, Nginx doesn't support using environment variables inside most configuration blocks.

But envsubst may be used as a workaround if you need to generate your nginx configuration dynamically before nginx starts.

Here is an example using docker-compose.yml:

image: nginx
volumes:
 - ./mysite.template:/etc/nginx/conf.d/mysite.template
ports:
 - "8080:80"
environment:
 - NGINX_HOST=foobar.com
 - NGINX_PORT=80
command: /bin/bash -c "envsubst < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" 

The mysite.template file may then contain variable references like this :

listen ${NGINX_PORT};

Update:

But you know this caused to its Nginx variables like this:

proxy_set_header        X-Forwarded-Host $host;

damaged to:

proxy_set_header        X-Forwarded-Host ;

So, to prevent that, i use this trick:

I have a script to run Nginx, that used on the docker-compose file as command option for Nginx server, i named it run_nginx.sh:

#!/usr/bin/env bash
export DOLLAR='$'
envsubst < nginx.conf.template > /etc/nginx/nginx.conf
nginx -g "daemon off;"

And because of defined new DOLLAR variable on run_nginx.sh script, now content of my nginx.conf.template file for Nginx itself variable is like this:

proxy_set_header        X-Forwarded-Host ${DOLLAR}host;

And for my defined variable is like this:

server_name  ${WEB_DOMAIN} www.${WEB_DOMAIN};

Also here, there is my real use case for that.

  • 2
    That kills nginx confs like proxy_set_header Host $http_host; – shredding Feb 17 '16 at 17:00
  • 20
    @shredding you can pass in variables names to be replaced - others are not touched: command: /bin/bash -c "envsubst '$VAR1 $VAR2' < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" works for me b/c I know what they are called ... – pkyeck Feb 19 '16 at 10:13
  • 3
    ok, forgot to escape the $, should be command: /bin/bash -c "envsubst '\$VAR1 \$VAR2' < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" – pkyeck Feb 19 '16 at 10:33
  • 2
    With consideration for escaping this works in your own Dockerfile: CMD ["/bin/sh","-c", "if [ -n \"${SOME_ENV}\" ]; then echo envsubst '${SOME_ENV}' with ${SOME_ENV} && envsubst '${SOME_ENV}' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf; fi ; nginx -g 'daemon off;'"] – KCD Nov 28 '16 at 0:12
  • 1
    This is a great solution. Thanks. Also the link mentioned above github.com/docker-library/docs/issues/496 has a great solution for the issue. – kabirbaidhya Feb 9 '17 at 17:48
29

Doing this with Lua is substantially easier than it sounds:

server {
    set_by_lua $server_name 'return os.getenv("NGINX_SERVERNAME")';
}

I found that here:

https://docs.apitools.com/blog/2014/07/02/using-environment-variables-in-nginx-conf.html

Edit:

Apparently this requires installing the lua module: https://github.com/openresty/lua-nginx-module

Edit 2:

Note that with this approach you have to define env variable in Nginx:

env ENVIRONMENT_VARIABLE_NAME

You have to do this in toplevel context in nginx.conf or it won't work! Not in server block or in config of some site in /etc/nginx/sites-available, because it is included by nginx.conf in http context (which is not top-level context).

Also note that with this approach if you try to make a redirect e.g.:

server {
    listen 80;
    server_name $server_name;
    return 301 https://$server_name$request_uri;
}

it won't work as well:

2016/08/30 14:49:35 [emerg] 1#0: the duplicate "server_name" variable in /etc/nginx/sites-enabled/default:8

And if you give a separate variable name to it:

set_by_lua $server_name_from_env 'return os.getenv("NGINX_SERVERNAME")';

server {
    listen 80;
    server_name $server_name_from_env;
    return 301 https://$server_name$request_uri;
}

nginx won't interpret it and will redirect you to https://%24server_name_from_env/.

  • 3
    you may need env NGINX_SERVERNAME somewhere in your nginx.conf. – hiroshi Feb 15 '16 at 14:28
  • This did not work for me, although I have the lua module in my nginx docker image. Could this be related to the fact that I include a config file within my nginx.conf? I was trying to set_by_lua the variable in the included config file, while the env MY_VAR declaration was in the main nginx.conf, as suggested. What a shame, this would have been the cleanest solution! – pederpansen May 16 '17 at 10:16
  • Does anyone know the pros/cons to using this method of envsubst? I guess the pro is you don't need to run the envsubstr command before starting the server and the con is that you need to install the lua module? I wonder if there are any security implications on either approach? – Mark Winterbottom Mar 4 '18 at 16:19
  • @MarkWinterbottom didn't test this yet but it looks like you would not have to grant write access to the nginx config files, which you have to using envsubst and which is a no-go in my case – Griddo Jun 5 '18 at 14:04
14

I wrote something that may or may not be helpful: https://github.com/yawn/envplate

It inline edits configuration files with ${key} references to environment variables, optionally creating backups / logging what it does. It's written in Go and the resulting static binary can be simply downloaded from the release tab for Linux and MacOS.

It can also exec() processes, substitute defaults, logs and has sensible failure semantics.

6

The official nginx image recommends using envsubst, but as pointed out by others it will replace also $host and other variables, which is not desirable. But fortunately envsubst can take as a parameter the names of variables to replace.

To avoid a very complex command parameter to the container (as in the linked example), you can write a Docker entrypoint script which will fill in the environment variables before executing the command. The entrypoint script is also a good place for validating the parameters and setting default values.

Here is an example of an nginx container which takes in API_HOST and API_PORT parameters as environment variables.

nginx-default.conf.template

resolver  127.0.0.11 valid=10s;  # recover from the backend's IP changing

server {
  listen  80;

  location / {
    root  /usr/share/nginx/html;
  }

  location /api {
    proxy_pass  http://${API_HOST}:${API_PORT};
    proxy_set_header  Host $http_host;
  }
}

docker-entrypoint.sh

#!/usr/bin/env sh
set -eu

envsubst '${API_HOST} ${API_PORT}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf

exec "$@"

Dockerfile

FROM nginx:1.15-alpine

COPY nginx-default.conf.template /etc/nginx/conf.d/default.conf.template

COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
5

What I did was to use the erb!

cat nginx.conf  | grep -i error_log

error_log <%= ENV["APP_ROOT"] %>/nginx/logs/error.log;

--After using erb

export APP_ROOT=/tmp

erb nginx.conf  | grep -i error_log

error_log /tmp/nginx/logs/error.log;

This is used in the Cloudfoundry staticfile-buildpack

Sample nginx configuration: https://github.com/cloudfoundry/staticfile-buildpack/blob/master/conf/nginx.conf

In your case

head /etc/nginx/nginx.conf
user www-data;
worker_processes 4;
pid /var/run/nginx.pid;
env APP_WEB_1_PORT_5000_TCP_ADDR;
upstream gunicorn {
    server $APP_HOST_NAME:$APP_HOST_PORT;
}

become

head /etc/nginx/nginx.conf
user www-data;
worker_processes 4;
pid /var/run/nginx.pid;
env <%= ENV["APP_WEB_1_PORT_5000_TCP_ADDR"] %>
upstream gunicorn {
    server <%= ENV["APP_HOST_NAME"] %>:<%= ENV["APP_HOST_PORT"] %>
}

#After applying erb

export APP_WEB_1_PORT_5000_TCP_ADDR=12.12.12.12
export APP_HOST_NAME=test
export APP_HOST_PORT=7089 

erb /etc/nginx/nginx.conf

head /etc/nginx/nginx.conf
user www-data;
worker_processes 4;
pid /var/run/nginx.pid;
env 12.12.12.12
upstream gunicorn {
    server test: 7089
}
  • 3
    Can you expand on this answer? It's worth noting that there is an already-accepted answer to this question above, but if you feel that your answer adds value to this then please expand on it. – BE77Y Feb 20 '15 at 8:59
4

Referring to the answer on using erb, it can be done as below.

Write the NGINX config file as an erb file containing the environment variable, and evaluate it using the erb command to a normal config file.

erb nginx.conf.erb > nginx.conf

Inside the server block of the nginx.conf.erb file there could be

listen <%= ENV["PORT"] %>;
  • 1
    Do I need to install Ruby then, inside the container? (If not, how is erb able to do variable substitution ... after the container was started, right?) — erb is this Ruby stuff right: stuartellis.name/articles/erb – KajMagnus Apr 8 '17 at 7:57
  • 1
    It's a command line tool that comes with standard Ruby installation. Try other options if you don't have it in the container. – Ruifeng Ma Apr 12 '17 at 11:45
3

I know this is an old question, but just in case someone stumbles upon this (as I now have), there's a much better way to do this. Because docker inserts the linked container's alias in /etc/hosts, you can just do

upstream upstream_name {
    server docker_link_alias;
}

assuming your docker command is something like docker run --link othercontainer:docker_link_alias nginx_container.

  • 1
    You can get the host using this method but not the port. You have to either hard code the port or assume it's using port 80. – Ben Whaley Mar 18 '16 at 13:58
2

Another option... I just found this tool today: https://github.com/kreuzwerker/envplate ... Written in Go, it can be installed very easily. Using it is quite simple. Though you will need to place template variables in your nginx.conf.

For example ${SOME_ENV_VAR} will be replaced when envplate's ep command is called on the file. So should your Dockerfile fail to get that binary or should it not run for some reason, it would lave your config invalid. Just a little note compared to other solutions like using perl or lua extensions.

I really like how you can set default values too for when the environment variable is not set. Ex. ${SOME_ENV_VAR:default-value} (and you can escape values). Again, envplate must still successfully run.

One benefit of using an approach like this is that you don't end up with a Docker image that's larger than necessary because you went off installing all sorts of extra modules you don't otherwise need. It may also be easier than using sed if things start to get complex and it contains that default value functionality.

2

I accomplish this using a shell script.

Here is the nginx template:

server {

    listen 80;
    server_name ___MY_DOMAIN_NAME___;
    charset utf-8;

    location /proxy {
        proxy_pass http://___PROXY_IP___:___PROXY_PORT___;
        proxy_set_header Host            $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }

    location / {
        return         200;
    }

}

And the script to replace the environment variables is here:

echo sleep 3
sleep 3

echo build starting nginx config


echo replacing ___MY_DOMAIN_NAME___/$MY_DOMAIN_NAME
echo replacing ___PROXY_IP___/$LETSENCRYPT_IP
echo replacing ___PROXY_PORT___/$PROXY_PORT

sed -i "s/___MY_DOMAIN_NAME___/$MY_DOMAIN_NAME/g" /etc/nginx/nginx.conf
sed -i "s/___PROXY_IP___/$PROXY_IP/g" /etc/nginx/nginx.conf
sed -i "s/___PROXY_PORT___/$PROXY_PORT/g" /etc/nginx/nginx.conf

cat /etc/nginx/nginx.conf

if [ -z "$MY_DOMAIN_NAME" ]; then
    echo "Need to set MY_DOMAIN_NAME"
    exit 1
fi  
if [ -z "$LETSENCRYPT_IP" ]; then
    echo "Need to set LETSENCRYPT_IP"
    exit 1
fi  
if [ -z "$LETSENCRYPT_PORT" ]; then
    echo "Need to set LETSENCRYPT_PORT"
    exit 1
fi
if [ -z "$LETSENCRYPT_HTTPS_IP" ]; then
    echo "Need to set LETSENCRYPT_HTTPS_IP"
    exit 1
fi 
if [ -z "$LETSENCRYPT_HTTPS_PORT" ]; then
    echo "Need to set LETSENCRYPT_HTTPS_PORT"
    exit 1
fi

nginx -g 'daemon off;'
1

Another possibility is to use the 'sed' command with regular expressions, then you won't have to mess with your config files at all! That way you can use your config files normally, but when you run docker, it will swap the values out with the env variables. None of this "add a string of text to your config files that you search and replace with."

You can create a run.sh file with replacement values using your environment variables.

To change the "7s" on this line:

client_body_timeout 7s;         #Default 60s

Sed Command using client_body_timeout as search line and $client_body_timeout as replacement env variable:

sed -i "s/\(client_body_timeout\).*\?\;/\1 $client_body_timeout;/" /usr/local/nginx/conf/nginx.conf

Copy/paste this line for every parameter you want to set and change the client_body_timeout with the config option and $client_body_timeout with the env variable it is associated with. Use with existing config file and it will just work.

0

Here's an example of using the sed approach, not necessarily better but it may be useful to some. First, add a custom keyword to be replaced within the conf file. Second, create a dockerfile that declares an ENV variable and then a CMD that uses sed to edit the config before running nginx explicitly.

So suppose your default.conf contains this with the keyword docker_host:

location /api { proxy_pass http://docker_host:9000/api; }

And write your Dockerfile similar to:

ENV docker_host localhost
ADD default.conf /etc/nginx/conf.d/default.conf
CMD sed -i.bak s/docker_host/$docker_host/g /etc/nginx/conf.d/default.conf &&   nginx -g "daemon off;"

Then build the image and run the container using

docker run -d -p 80:80 -e "docker_host=${env:COMPUTERNAME}" imagename
-1

You should able to do what you want with Dockerize's templates.

  • 6
    Can you add some more details? – Pierre.Vriens May 20 '16 at 11:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.