Serving Django App Statically at The Lowest Cost Possible

02 November 2019

Two months ago, I was planning to publish a mobile app for both Android and iOS devices. However, I didn’t know how to code mobile apps natively. I found that there is an app type called “Webview” in which you just prepare a mobile friendly website and serve it inside the app. That was cool. So I coded the web application with Django framework. I just need to install it to a server, set a domain name and go. But what if I have thousands of active users in the future, how much resources will I need. Also, what if my competitors conduct DDOS attacks, will I have time or budget to deal with it? Answer was no. So I need some alternative methods.

Basically, web application were doing following things:

  • Gather stats about football games from an API
  • Do predictions with precomputed machine learning model
  • Serve results inside the app

The first thing came into my mind was using a serverless architecture. I’m a big fan of that concept. It reduces the cost, attack surface, maintenance struggles. I coded few AWS Lambda functions before, for 10-15 users. But when I calculated the cost of thousands of users, it wasn’t cheap as I thought. Lambda+API Gateway would cost higher than $40 per month.

I’m also big fan of static websites. It reduces the cost and attack surface much higher than the serverless architecture. For example, I’m publishing this blog statically via AWS S3 by using Jekyll framework. Since my Django app has no user interaction, I thought maybe I can serve it as a static website as well.

I researched it a lot, tried couple of open source projects but no luck. I always encountered problems. There was no stable solution for converting a Django app to a static website. So I decided to make my own process. After some trials, my old friend httrack was the most stable solution.

So I created a Python script locally which covers everything. The overall process was like that:

  • Gather data, do secret calculations and write results to Django’s sqlite database:
with open('games.json') as json_file:
    data = json.load(json_file)
    for i in data.items():
        cursor.execute("INSERT INTO deepapp_bet(home_team,away_team,date,country,pick) VALUES(?,?,?,?,?)",
            (i[1]['home_team'],i[1]['away_team'],i[1]['date'],i[1]['country'],i[1]['pick']))   
  • Run Django web server locally:
proc = subprocess.Popen(["python3","/opt/deepscore/manage.py","runserver"])
  • Clone Django website with httrack
d = subprocess.check_output(['/usr/bin/httrack "http://127.0.0.1:8000" -O ' +base_dir+'/out -*.png -*.css -*.svg'],shell=True)
  • I had to change URLs inside the HTML files with http://127.0.0.1:8000 with my production URL:
def findReplace(directory, find, replace, filePattern):
    for path, dirs, files in os.walk(os.path.abspath(directory)):
        for filename in fnmatch.filter(files, filePattern):
            filepath = os.path.join(path, filename)
            with open(filepath) as f:
                s = f.read()
            s = s.replace(find, replace)
            with open(filepath, "w") as f:
                f.write(s)

findReplace(base_dir+"/out/127.0.0.1_8000", "http://127.0.0.1:8000", "https://deepscoreapp.com", "*.html")
findReplace(base_dir+"/out/127.0.0.1_8000/match", "http://127.0.0.1:8000", "https://deepscoreapp.com", "*.html")                
  • Send data to S3 bucket:
d = subprocess.check_output(['/usr/local/bin/aws s3 sync . s3://prodwebsite.com'],shell=True,cwd=base_dir+"/out/127.0.0.1_8000")

Bonus: Redirecting Users Based on Their Country With Cloudflare Workers

There is one thing I didn’t mention in the first part. If user is located in Turkey, I need to redirect zhe to website.com/tr. It was possible with the Django app, but I can’t run any code since whole website lies statically on a S3 bucket. Cloudflare workers was the solution for that. By writing some Javascript code, you can achieve the same goals with the original Django application.

This my script which redirects users to specific URIs, according to their countries

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  let url = new URL(request.url)
  let path = url.pathname.split('/')[1]
  let country = request.headers.get('CF-IpCountry')
  if (path == "static" || path == "media"){
    return fetch(url)
  }
  if (country == 'TR') {
    if (path == "match"){
      return fetch('https://deepscoreapp.com/tr/match/'+url.pathname.split('/')[2])
    }
    else{
      return fetch('https://deepscoreapp.com/tr/'+path)
    }
      
    
  }
  else{
    if (path == "match"){
      return fetch('https://deepscoreapp.com/match/'+url.pathname.split('/')[2])
    }
    else{
      return fetch('https://deepscoreapp.com/'+path)
    }
  }
}

Results

I got 346,700 requests yesterday and Cloudflare cached most of them (images, css etc.)

Cloudflare Stats

And my monthly S3 cost is too low 😍:

Cloudflare Stats

The mobile app’s name is “Deepscore”, you can download it from both Google Play and App Store if you are interested in football predictions :)