---
title: "Django behind a proxy: Fixing absolute URLs\n    | Ubuntu"
description: I recently tried to setup OpenID for one of our sites to support authentication
  with login.ubuntu.com, and it took me much longer than I’d anticipated because our
  site is behind a reverse-proxy. My problem I was trying to setup OpenID with the
  django-openid-auth plugin. Normally our sites don’t include absolute links (https://example.com/
  […]
url: https://ubuntu.com/blog/django-behind-a-proxy-fixing-absolute-urls?format=md
keywords: index, follow
---

1. [Blog](https://ubuntu.com/blog)
2. Article

---

[Robin Winslow](https://ubuntu.com/blog/author/nottrobin "More about Robin Winslow")

18 August 2015

# Django behind a proxy: Fixing absolute URLs

[Design](https://ubuntu.com/blog/tag/design)

---

Share the article

I recently tried to setup OpenID for one of our sites to support authentication with [`login.ubuntu.com`](https://login.ubuntu.com/), and it took me much longer than I’d anticipated because our site is behind a [reverse-proxy](https://en.wikipedia.org/wiki/Reverse_proxy).

## My problem

I was trying to setup OpenID with the [django-openid-auth plugin](https://launchpad.net/django-openid-auth). Normally our sites don’t include absolute links (`https://example.com/hello-world`) back to themselves, because relative URLs (`/hello-world`) work perfectly well, so normally Django doesn’t need to know the domain name that it’s hosted it.

However, when authenticating with OpenID, our website needs to send the user off to `login.ubuntu.com` with a [callback url](http://stackoverflow.com/questions/23347056/what-is-a-callback-url-in-relation-to-an-api) so that once they’re successfully authenticed they can be directed back to our site. This means that the `django-openid-auth` needs to ask Django for an absolute URL to send off to the authenticator (e.g. `https://example.com/openid/complete`).

## The problem with proxies

In our setup, the Django app is served with a light [Gunicorn server](http://gunicorn.org/) behind an [Apache](https://en.wikipedia.org/wiki/Apache_HTTP_Server) front-end which handles HTTPS negotiation:

```
User <-> Apache <-> Gunicorn (Django)
```

(There’s actually an additional [HAProxy load-balancer](http://www.haproxy.org/) in between, which I thought was complicating matters, but it turns out HAProxy was just passing through requests absolutely untouched and so was irrelevant to the problem.)

Apache was setup as a [reverse-proxy](https://en.wikipedia.org/wiki/Reverse_proxy) to Django, meaning that the user only ever talks to Apache, and Apache goes off to get the response from Django itself, with Django’s local network IP address – e.g. `10.0.0.3`.

It turns out this is the problem. Because Apache, and not the user directly, is making the request to Django, Django sees the request come in at `http://10.0.0.3/openid/login` rather than `https://example.com/openid/login`. This meant that `django-openid-auth` was generating and sending the wrong callback URL of `http://10.0.0.3/openid/complete` to `login.ubuntu.com`.

## How Django generates absolute URLs

`django-openid-auth` uses [`HttpRequest.build_absolute_uri`](https://docs.djangoproject.com/en/1.8/ref/request-response/#django.http.HttpRequest.build_absolute_uri) which in turn uses [`HttpRequest.get_host`](https://docs.djangoproject.com/en/1.8/ref/request-response/#django.http.HttpRequest.get_host) to retrieve the domain. `get_host` then normally uses the `HTTP_HOST` header to generate the URL, or if it doesn’t exist, it uses the request URL (e.g.: `http://10.0.0.3/openid/login`).

However, after inspecting [the code for `get_host`](https://github.com/django/django/blob/1.7.9/django/http/request.py#L62) I discovered that if and only if `settings.USE_X_FORWARDED_HOST` is `True` then Django will look for the `X-Forwarded-Host` header first to generate this URL. This is the key to the solution.

## Solving the problem – Apache

In our Apache config, we were initially using [mod\_rewrite](http://httpd.apache.org/docs/current/mod/mod_rewrite.html) to forward requests to Django.

```
RewriteEngine On
RewriteRule ^/?(.*)$ http://10.0.0.3/$1 [P,L]
```

However, when proxying with this method Apache2 doesn’t send the `X_Forwarded_Host` header that we need. So we changed it to use `mod_proxy`:

```
ProxyPass / http://10.0.0.3/
ProxyPassReverse / http://10.0.0.3/
```

This then means that Apache will send three headers to Django: `X-Forwarded-For`, `X-Forwarded-Host` and `X-Forwarded-Server`, which will contain the information for the original request.

In our case the Apache frontend used HTTPS protocol, whereas Django was only using so we had to pass that through as well by manually setting Apache to pass an `X-Forwarded-Proto` to Django. Our eventual config changes looked like this:

```
<VirtualHost *:443>
    ...
    RequestHeader set X-Forwarded-Proto 'https' env=HTTPS

    ProxyPass / http://10.0.0.3/
    ProxyPassReverse / http://10.0.0.3/
    ...
</VirtualHost>
```

This meant that Apache now passes through all the information Django needs to properly build absolute URLs, we just need to make Django parse them properly.

## Solving the problem – Django

By default, Django ignores all `X-Forwarded` headers. As mentioned earlier, you can set `get_host` to read the `X-Forwarded-Host` header by setting `USE_X_FORWARDED_HOST = True`, but we also needed one more setting to get HTTPS to work. These are the settings we added to our Django `settings.py`:

```
# Setup support for proxy headers
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
```

After changing all these settings, we now have Apache passing all the relevant information (`X-Forwarded-Host`, `X-Forwarded-Proto`) so that Django is now able to successfully generate absolute URLs, and `django-openid-auth` now works a charm.

[Get in touch

Interested in running Ubuntu in your organization?](https://ubuntu.com/about/contact-us/form)

## Newsletter signup

Get the latest Ubuntu news and updates in your inbox.

Work email:

\*I agree to receive information about Canonical's
products and services.

By submitting this form, I confirm that I have read and agree to [Canonical's Privacy Policy](https://canonical.com/legal/data-privacy).

Sign up

## Share on

---

## Related posts

[### Challenges designers face in open source (and how to fix them)](https://ubuntu.com/blog/challenges-designers-face-in-open-source-and-how-to-fix-them)

Open source powers up to 90% of modern software, yet many projects lack usability. Canonical’s Design team surveyed 115 cross-functional professionals to uncover the 4 core...

[Kola Ojoodide](https://ubuntu.com/blog/author/kola-ojoodide)

26 June 2026

[### Template: Streamlining open source design contributions](https://ubuntu.com/blog/template-streamlining-open-source-design-contributions)

As designers working at Canonical, we’re always thinking about open source. We believe that encouraging more designers to contribute to open source benefits everyone, from the...

[Nina Rojc](https://ubuntu.com/blog/author/ninarojc)

16 June 2026

[### Decoding design: How design and engineering thrive together in open source](https://ubuntu.com/blog/decoding-design-how-design-and-engineering-thrive-together-in-open-source)

Open source thrives on engineering-driven processes. Fast feedback loops, terminal tools, Git workflows: they’re the lifeblood of how we build software in the open. But for...

[Miguel Divo](https://ubuntu.com/blog/author/mdivo)

22 May 2026

[### Why web engineering is great](https://ubuntu.com/blog/why-web-engineering-is-great)

Hi, I’m Johann! I’m an engineering manager in Canonical’s web team. For the larger part of my 15 years of work experience as engineer, I’ve been working in web development....

[Johann Wolf](https://ubuntu.com/blog/author/johannwolf)

27 April 2026
