Django and nginx file proxy

In this blog post series, I will show you how to use Nginx for hiding download urls. Django will serve us as a backend. Let’s go!

In this series I will build simple web application - user upload a file via api and then she/he wants to download it. But as a creator of this service I decided to not show my url to end user - instead I want to use a proxy.

Setting up Django & Nginx application in docker

In this blog post, I will setup django with Nginx using docker containers. If you want to know how to use Nginx for hiding download urls wait till next week.

I have my django application up and running in docker with following structure:

.
├── compose
│   ├── django
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
├── config
│   ├── __init__.py
│   ├── settings
│   │   ├── common.py
│   │   ├── __init__.py
│   │   ├── local.py
│   ├── urls.py
│   └── wsgi.py
├── django_nginx_proxy
│   ├── images
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── __init__.py
│   │   ├── migrations
│   │   │   ├── 0001_initial.py
│   │   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── media
├── docker-compose.yml
├── LICENSE
├── Makefile
├── manage.py
├── README.rst
└── requirements
  ├── base.txt
  └── local.txt

It’s one app - Images with stores information about image - title and image_file.

To add nginx I have to create a new subfolder in compose directory - nginx with Dockerfile:

FROM nginx:latest

ADD nginx.conf /etc/nginx/nginx.conf

RUN mkdir -p /var/www/media

WORKDIR /var/www/media

RUN chown -R nginx:nginx /var/www/media

It’s using the latest nginx and copies it configuration. Then make sure that nginx user has access to interesting for us folder. nginx.conf is presenting as follows:

user  nginx;

http {

  client_max_body_size 100M;


  upstream app {
    server django:5000;
  }

  server {
    listen 80;
    charset     utf-8;

    location /media/ {
      root /var/www/;
    }

    location / {
      try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass   http://app;

    }
  }
}

I present you the most important lines - the rest you will find in a repo. Setting client_max_body_size allows me to upload files till 100M. I use nginx to serve media files - uploaded images. That’s why I needed location /media/.

The rest of requests goes to django application - and in production settings - gunicorn.

The last piece of a puzzle is docker-compose.yml:

nginx:
  build: ./compose/nginx
  depends_on:
    - django
  ports:
    - "0.0.0.0:80:80"
  volumes:
    - ./django_nginx_proxy/media:/var/www/media

This config tells docker-compose to build nginx from Dockerfile under compose/nginx.

Important line here is volumes - I use only one folder in nginx container. Thanks to that we user upload a file it goes from django container to media folder and then is taken up by nginx container.

How to hide urls from the user?

It can be done in several ways but I will show it how you can use a power of Nginx to do that.

When the user uses my API I will serve him a generic link to download an image: /download/image/<image_id>. Under the hood, Django will add a header called X-Accel-Redirect to the server response. This header will tell Nginx that media files are served from internal location. The user will see the only first link, not the hidden one!

How to use X-Accel-Redirect with Django?

First of all, I want my media location to be internal. It means that Nginx will allow access only when the location is accessed via redirection. To enable that I have to edit nginx.conf and add internal:

location /media/ {
  internal;
  root /var/www/;
}

I want my API to return image_link which will be generic url in this form: /download/image/<image_id>. How to do that? Add new field in serializers:

from rest_framework.reverse import reverse


class ImageSerializer(serializers.ModelSerializer):
    image_link = serializers.SerializerMethodField('get_url')

  # rest of the Meta

    def get_url(self, obj):
        request = self.context['request']
        return reverse('api:download-image', kwargs={'image_id': obj.id}, request=request)

At the end of get_url I’m reversing the user to the new view download_image_view:

from django.http import HttpResponse


def download_image_view(request, image_id):
    image = Image.objects.get(id=image_id)
    response = HttpResponse()
    response['X-Accel-Redirect'] = image.image_file.url
    response['Content-Disposition'] = 'attachment; filename="{}"'.format(image.image_file.name)
    return response

The most important lines here are those two that adds headers to the response. First I use mentioned before X-Accel-Redirect with media location. Right after that, I add Content-Disposition header. It tells a browser that this file should be downloaded with provided filename.

That’s all! Right now user can only use download/image url, not the media one.

Source code is available in this repo.