---
title: 'Django behind a proxy: Fixing absolute URLs'
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://canonical.com/blog/django-behind-a-proxy-fixing-absolute-urls?format=md
---

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

---

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

on 18 August 2015

# Django behind a proxy: Fixing absolute URLs

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

Share on:* [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://www.canonical.com/blog/django-behind-a-proxy-fixing-absolute-urls "Share on Facebook")
* [Twitter](https://twitter.com/share?text=Django%20behind%20a%20proxy%3A%20Fixing%20absolute%20URLs&url=https://www.canonical.com/blog/django-behind-a-proxy-fixing-absolute-urls&hashtags=ubuntu "Share on Twitter")
* [LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https://www.canonical.com/blog/django-behind-a-proxy-fixing-absolute-urls&title=Django%20behind%20a%20proxy%3A%20Fixing%20absolute%20URLs "Share on LinkedIn")

---

---

## Newsletter signup

Get the latest Canonical 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

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.

#### Related posts

---

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

26 June 2026

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

Company

Ubuntu tech blog

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 challenges UI/UX designers face when contributing, and how maintainers can solve them. ...

---

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

16 June 2026

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

Design

Article

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 project maintainers to the end users themselves.   In the 2025 edition of FOSSBackstage conference, we presented our research findings on  why designers don’t get ...

---

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

22 May 2026

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

Design

Article

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 software to truly excel, we need to create user experiences that empower people to use them. I wanted to bring this conversation into the spotlight as part of Canonica ...
