8

On production I use the chain Django - UWSGI - Docker - Nxing. UWSGI works with the port 50012 and Ngxin is configured as:

proxy_pass http://localhost:50012;

Django process thinks that its host is localhost:50012 instead of the domain that Nginx listens to. So when the function build_absolute_uri is called there's localhost:50012 instead of my domain. Is there a way to make Django use the custom host name when build_absolute_uri is called?

Notice: in some libraries build_absolute_uri called implicitly (like social-django, or example), so avoiding this function is not a solution in my case.

Fomalhaut
  • 8,590
  • 8
  • 51
  • 95
  • I guess you want to take a look at the [sites framework](https://docs.djangoproject.com/en/2.2/ref/contrib/sites/). Are you sure none of the answers at [this question](https://stackoverflow.com/questions/2345708/how-can-i-get-the-full-absolute-url-with-domain-in-django) would apply here? – Paulo Scardine Sep 21 '19 at 22:30
  • @PauloScardine How is it supposed to help? In the documentation, it's said, this framework should be used in case of several domains and for associating objects with each of them. In my case I don't care of any objects and I have exactly one domain. – Fomalhaut Sep 21 '19 at 22:40
  • Specifically ["Getting the current domain for full URLs"](https://docs.djangoproject.com/en/2.2/ref/contrib/sites/#getting-the-current-domain-for-full-urls) on that page. – Paulo Scardine Sep 21 '19 at 22:43
  • @PauloScardine I do not think it relates to what I asked about... – Fomalhaut Sep 21 '19 at 22:47
  • 3
    Set `USE_X_FORWARDED_HOST = True` in settings and configure nginx to pass the header along. Otherwise django will use `request.META['HTTP_HOST']` or `request.META['SERVER_NAME']`. The nginx part of the setup probably is best suited to another site in the network as it is more about infrastructure management than about programming. – Paulo Scardine Sep 21 '19 at 22:57

3 Answers3

11

The problem

When the public hostname you use to reach the proxy differ from the internal hostname of the application server, Django has no way to know which hostname was used in the original request unless the proxy is passing this information along.

Possible Solutions

1) Set the proxy to pass along the orginal host

From MDN:

The X-Forwarded-Host (XFH) header is a de-facto standard header for identifying the original host requested by the client in the Host HTTP request header.

Host names and ports of reverse proxies (load balancers, CDNs) may differ from the origin server handling the request, in that case the X-Forwarded-Host header is useful to determine which Host was originally used.

There are two things you should do:

  1. ensure all proxies in front of Django are passing along the X-Forwarded-Host header
  2. turn on USE_X_FORWARDED_HOST in the settings
  3. if the internal and external scheme differ as well, set SECURE_PROXY_SSL_HEADER to a meaningful value and set the server to send the corresponding header

When USE_X_FORWARDED_HOST is set to True in settings.py, HttpRequest.build_absolute_uri uses the X-Forwarded-Host header instead of request.META['HTTP_HOST'] or request.META['SERVER_NAME'].

I will not delve too much into the proxy setup part (as it is more related to professional network administration than to programming in the scope of this site) but for nginx it should be something like:

location / {
    ...
    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    ...
    proxy_pass http://upstream:port;
}    

Probably the best solution as it is fully dynamic, you don't have to change anything if the public scheme/hostname changes in the future.

If the internal and external scheme differ as well you may want to set SECURE_PROXY_SSL_HEADER in settings.py to something like this:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

And then add the following to the server config:

proxy_set_header X-Forwarded-Proto https;

2) Use the same hostname for public and private servers

Lets say your public hostname is "host.example.com": you can add a line like this to your /etc/hosts (on Windows %windir%\System32\drivers\etc\hosts):

127.0.0.1    host.example.com

Now you can use the hostname in the nginx config:

proxy_pass http://host.example.com:port;

When the internal and external scheme differ as well (external https, internal http), you may want to set SECURE_PROXY_SSL_HEADER as described in the first solution.

Every time the public hostname changes you will have to update the config but I guess this is OK for small projects.

Vassilis
  • 2,878
  • 1
  • 28
  • 43
Paulo Scardine
  • 73,447
  • 11
  • 124
  • 153
  • 2
    Thank you, this worked very well in my case! One optional thing I would add to your answer would be `X-Forwarded-Proto: https` if needed. – Fomalhaut Sep 21 '19 at 23:39
  • Answer updated with recommended `SECURE_PROXY_SSL_HEADER` setting for when internal and external schemes are also different. – Paulo Scardine Sep 22 '19 at 01:09
2

I got mine working using proxy_redirect

Lets say you have a container or an upstream with the name app and you want it to return 127.0.0.1 as the host, Then your config should include:

server {
  listen 80;

  location / {
    proxy_pass http://app:8000;

    proxy_redirect http://app:8000 http://127.0.0.1:8000;
  }
}

Here's my final config:

server {
  listen 80;

  location / {
    proxy_pass http://app:8000;

    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_redirect http://app:8000 http://127.0.0.1:8000;
  }
}

Also checkout this article for a detailed explanation https://mattsegal.dev/nginx-django-reverse-proxy-config.html

Divine Hycenth
  • 620
  • 6
  • 11
1

I had a problem same as the question, I used nginx, gunicorn and django on production without docker.

get_current_site(request).domain

returned localhost so I have some issue in drf yasg base url. Simply I solved it by adding

include proxy_params;

to nginx conf.

mastisa
  • 1,875
  • 3
  • 21
  • 39