111

I am using nginx/0.7.68, running on CentOS, with the following configuration:

server {
    listen       80;
    server_name ***;
    index index.html index.htm index.php default.html default.htm default.php;

    location / {
            root   /***;
            proxy_pass   http://***:8888;
            index  index.html index.htm;
    }
    # where *** is my variables

The proxy_pass is to a DNS record whose IP changes frequently. Nginx caches the outdated IP address, resulting in a request to the wrong IP address.

How can I stop nginx from caching the IP address, when it is outdated?

xiamx
  • 1,302

6 Answers6

163

Accepted answer didn't work for me on nginx/1.4.2.

Using a variable in proxy_pass forces re-resolution of the DNS names because NGINX treats variables differently to static configuration. From the NGINX proxy_pass documentation:

Parameter value can contain variables. In this case, if an address is specified as a domain name, the name is searched among the described server groups, and, if not found, is determined using a resolver.

For example:

server {
    ...
    resolver 127.0.0.1;
    set $backend "http://dynamic.example.com:80";
    proxy_pass $backend;
    ...
}

Note: A resolver (i.e. the name server to use) MUST be available and configured for this to work (and entries inside a /etc/hosts file won't be used in a lookup).

By default, version 1.1.9 or later versions of NGINX cache answers using the TTL value of a response and an optional valid parameter allows the cache time to be overridden:

resolver 127.0.0.1 [::1]:5353 valid=30s;

Before version 1.1.9, tuning of caching time was not possible, and nginx always cached answers for the duration of 5 minutes..

ohaal
  • 2,352
  • 2
  • 20
  • 21
  • wouldn't this force a dns query on every single request? that sounds like awful performance... – lucascaro Mar 10 '15 at 03:42
  • No, read the source. In such setup ip address of "foo.example.com" will be looked up dynamically and result will be cached for 5 minutes. I've added it to the answer for clarity. – ohaal Mar 10 '15 at 09:43
  • 24
    After spending most of my day on this - on Ubuntu 12.04 with nginx 1.1.19, set inside location doesn't work properly. Beware – omribahumi Jul 19 '15 at 13:38
  • 1
    This solution worked with me, however I couldn't find a reference for the 5 minutes TTL. http://nginx.org/en/docs/http/ngx_http_core_module.html#resolver By default, nginx caches answers using the TTL value of a response. An optional valid parameter allows overriding it: resolver 127.0.0.1 [::1]:5353 valid=30s; – Montaro Aug 15 '15 at 02:51
  • It could be wrong, it's based on the source link I provided in my answer. In such setup ip address of "foo.example.com" will be looked up dynamically and result will be cached for 5 minutes. – ohaal Aug 15 '15 at 17:09
  • I just implemented this. I'm doing set in server, not location. – jorfus Aug 31 '16 at 22:30
  • 15
    Note: for docker, it's DNS resolver resides at 127.0.0.11, so for development, I use this: resolver 127.0.0.11 [::1]:5353 valid=15s; – Dalibor Filus Jun 01 '18 at 11:18
  • Found more references to the 5 min default cache expiry in old versions in this patch http://mailman.nginx.org/pipermail/nginx-devel/2011-November/001466.html. Current source here. https://github.com/nginx/nginx/blob/27b3d3dcca5fcc82350a823881f3d06161327b59/src/core/ngx_resolver.c#L199 – David Xia May 07 '19 at 02:09
  • @omribahumi 4 years and this comment is still useful. I was getting 500 from nginx, and had no clue. – Ashwani Agarwal Jul 09 '19 at 06:24
  • 9
    Here's a really nasty gotcha that lost me over a day trying to debug: If you use a variable in the proxy_pass directive, the interpretation of the path changes. When you do this, the path of the original request is completely overridden and replaced by the path given in proxy_pass. You must use a proxy_pass without a path in the url in order to avoid this behaviour. – Sam Svenbjorgchristiensensen Mar 02 '20 at 01:02
  • 5
    I had to change it to set $backend "http://dynamic.example.com:80$request_uri";, otherwise the request path will be empty (as the comment above me also mentions) – gilad905 Dec 09 '20 at 12:41
  • FYI support for set in stream blocks was added in 1.19. Was not possible before. – iTayb Jan 03 '21 at 11:39
  • Does this work with upstreams? Ie, if I have an upstream upstream1 and change my config from proxy_pass https://upstream1 to set $somevar upstream1; proxy_pass https://$somevar can I get similar results? – a p Apr 23 '21 at 18:07
  • "Note: A resolver (i.e. the name server to use) MUST be available ", for those not aware this means you need to set up a DNS server. dnsmasq is easy to setup (https://wiki.debian.org/dnsmasq). – Joseph Palermo Jun 18 '21 at 14:54
  • Won't this kill performance to be performing a DNS lookup on every single request? – MrMesees Oct 27 '21 at 04:13
  • 1
    @MrMesees read the part about cache – ohaal Oct 28 '21 at 13:52
  • This worked very well for me. In my case with Kubernetes..I added in the server {} stanza resolver 10.96.0.10 valid=30s; (That is the common IP for Weavworks dns resolver). Then I set a variable for just the host also in server{} set $backend_host "nextcloud-service.nextcloud.svc.cluster.local"; . Finally in the server{location{}} stanza I add proxy_pass http://$backend_host:8129; – Lon Kaut Mar 16 '22 at 15:17
  • 4
    I found that if the hostname being resolved is just a shortname (no domain part), as would likely be the case for Docker service discovery by service hostname, then your need to add a trailing dot, e.g. set $backend_host "http://backend.:1234/abc";. Otherwise the hostname is resolved once at startup, as though a variable was not being used. – Terry Burton Oct 08 '22 at 01:30
  • 1
    @LonKaut regarding Kubernetes resolver, the 10.96.0.10 IP is a dynamic address (meaning I have a different address in my cluster). I've created custom entrypoint for nginx dockerfile that replaces conf values with envsubst and I fetch resolver IP with nslookup kubernetes.default | grep -E '^Server:\s+' | sed -E 's/^Server:\s+//' (must be run during container startup, inside the Pod). Note: dnsutils (ubuntu) must be installed in the container. – Jan Święcki Nov 27 '22 at 13:38
  • On Ubuntu servers, resolver is 127.0.0.53. – Dale C. Anderson Jan 06 '23 at 22:55
15

There is valuable information in gansbrest comment and ohaal answer.

But I think it's important to mention this official nginx article, posted in 2016, it clearly explains nginx behaviour on this matter and the possible solutions: https://www.nginx.com/blog/dns-service-discovery-nginx-plus/

We indeed have to "Set the Domain Name in a Variable" and use the resolver directive.

however, using a variable changes the rewrite behaviour. You may have to use the rewrite directive, it depends on your location and proxy_pass setup.

PS: would have post a comment but not enough points yet...

Jack B.
  • 151
12

ohaal's answer takes most of us there, but there is a case where the DNS resolver does not live at 127.0.0.1 (eg when you're in a special containerized environment)

In that case, you may want to change the nginx conf to resolver ${DNS_SERVER};. Then, before you start nginx, run

export DNS_SERVER=$(cat /etc/resolv.conf |grep -i '^nameserver'|head -n1|cut -d ' ' -f2)
envsubst '${DNS_SERVER}' < your_nginx.conf.template > your_nginx.conf

Note, that you need the gettext package installed, as that provides the envsubst command.

Moritur
  • 103
wonton
  • 221
11

It's an intriguing question and AFAIK that's not going to work well. You can try to use the upstream module and use the directives for failover to see if it works as a hack.

2018 edit: a lot of things changed. Check the answer by @ohaal to get real information about this.

Anon
  • 1,255
coredump
  • 12,771
  • 1
    surprisingly when i changed to upstream, everything worked as expected. I'll then mark this as correct answer – xiamx Feb 27 '11 at 22:20
  • 1
    According to the documentation, there's a special upstream server flag resolve that's only available in the commercial version (see http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server) – omribahumi Jul 19 '15 at 13:48
  • 1
    @gansbrest that site seems to be some kind of spammy site? i'd ask that you remove your response. – chizou Jun 28 '18 at 00:02
  • It's a shame that in 2020 the upstream module still doesn't fix this issue. Resolving the address once and using that forever is useless. – Phil Sep 03 '20 at 08:23
2

I've hacked together a script to watch a conf.d folder upstreams for dns changes and reload nginx upon detection. It's a first pass, and surely can be improved (next pass, I'll use nginx -T to parse upstreams specifically. Same idea could be used for proxy_pass directives):

#!/bin/bash

get_upstreams() {
  local files=$@
  grep -hEo '(server\s+)[^:;]+' $files | cut -d' ' -f 2
}

resolve_hosts() {
  local hosts=$@
  for h in $hosts; do dig +short $h; done | sort -u
}

watch_dir=$1

[ -d $watch_dir ] || exit 2

upstreams=$(get_upstreams $watch_dir/*)
ips=$(resolve_hosts $upstreams)
if [ ! "$ips" ]; then
  echo "Found no resolvable hosts in $watch_dir files."
fi

host_hash=$(echo $ips | /usr/bin/sha512sum)

echo $host_hash
echo $ips

while [ -d $watch_dir ]; do
  sleep 30
  upstreams=$(get_upstreams $watch_dir/*)
  ips=$(resolve_hosts $upstreams)
  new_hash=$(echo $ips | /usr/bin/sha512sum)
  if [ "$host_hash" != "$new_hash" ]; then
    echo Detected an upstream address change.  $ips
    echo Reloading nginx
    echo $new_hash
    echo $ips
    /sbin/service nginx reload
    host_hash=$new_hash
  fi
done
1

The most voted answer from @ohaal didn't work for me. I had to use a variant of the proposed solution:

resolver 127.0.0.11 ipv6=off valid=10s;

location ~ ^/SomePath/(.*)$ { proxy_pass http://dynamic.example.com:80/SomePath/$1$is_args$args; }

location ~ ^/Other/Path/(.*)$ { proxy_pass http://dynamic.example.com:80/Other/Path/$1$is_args$args; }

Unlike the other solution, this works also of for urls like:

http://proxied:80/SomePath/bla
http://proxied:80/SomePath/bla/bla?whatever=123
http://proxied:80/Other/Path/bla
http://proxied:80/Other/Path/bla/bla

See https://stackoverflow.com/a/8130872/948938