Hosting Multiple Domains on Python Flask

No, we will not use host matching! Instead, we’ll use our own architecture.

The title makes the content of this article obvious, so let’s jump right into it. This method might be a little tedious for high-traffic websites, but if you have a handful of low-traffic websites and want to host them under a single entity (such as a Heroku dyno), this might save you the cost of registering a distinct entity for each website.

Creating the skeleton project

We start by creating a regular Python folder with VENV and whatnot.

Next, we import Flask and Waitress using pip. You can pick another web server (such as Gunicorn) if you like.

We create the following file / folder structure under the project folder.

  • /
    • app.py
    • domain.json
    • static
      • domain1
        • index.html
      • domain2
        • index.html
    • templates
      • domain3
        • index.html
        • about.html
        • contact.html

app.py is the entry point of the website. You can rename it if you like.

domain.json will contain paths for each domain.

“Static” and “Templates” folders are required by Flask. You can pick other names for those folders, but let’s keep things simple. We are assuming that two domains will be hosted with static files, and one will be hosted with page templates. You can mix and match however you want.

domain1 / 2 / 3 are obviously example folder names.

domain.json

Structure of this file will look like this:

{
    "domains": [
        {
            "hostname": "domain1.com",
            "paths":
                [{ "path": "",
                   "static": "domain1/index.html" }]
        },
        {
            "hostname": "domain2.com",
            "paths":
                [{ "path": "",
                   "static": "domain2/index.html" }]
        },
        {
            "hostname": "domain3.com",
            "paths": [
                { "path": "",
                  "template": "domain3/index.html" },
                { "path": "about",
                  "template": "domain3/about.html" },
                { "path": "contact",
                  "template": "domain3/contact.html" }
            ]
        }
    ]
}

The content of this file is pretty intuitive. For each domain, we are enlisting the paths and where they should point to. Note that I gave different JSON tags to separate static folder from templates.

In this example;

  • domain1.com will point to /static/domain1/index.html
  • domain2.com will point to /static/domain2/index.html
  • domain3.com will point to /template/domain3/index.html
  • domain3.com/about will point to /template/domain3/about.html
  • domain3.com/contact will point to /template/domain3/contact.html

app.py

Here is where the magic happens. Check the following example code:

import os
import json
from urllib.parse import urlparse
from flask import Flask, render_template, request
from waitress import serve

##############################
# APPLICATION
##############################

_APP = Flask(__name__)

@_APP.route("/", defaults={'path': ''}, methods=['GET', 'POST'])
@_APP.route("/<path:path>", methods=['GET', 'POST'])
def any_route(path):
    # Known paths
    hostname = urlparse(request.base_url).hostname

    if hostname == "localhost":
        pass # hostname = "domain1.com" - for local tests

    with open("domain.json") as domain_file:
        domains = json.load(domain_file)

    for domain in domains["domains"]:
        if domain["hostname"] not in hostname:
            continue
        for domain_path in domain["paths"]:
            if path != domain_path["path"]:
                continue
            if "template" in domain_path:
                return render_template(domain_path["template"])
            if "static" in domain_path:
                return _APP.send_static_file(domain_path["static"])

    # Unknown path
    return "Page not found"

##############################
# STARTUP
##############################

if __name__ == "__main__":
    serve(_APP, port=5000)

You can extend this file to read environment values, do security checks, generate JSON files, etc.; but you get the basic idea. We have a single entry point (any_route), which extracts the domain name and reads domain.json to determine what should be rendered & returned.

Links in html files

In /static/domain1/index.html, links to static files would look like this:

<link rel="stylesheet" href="/static/domain1/style.css">
<img src="/static/domain1/logo.png">

In /template/domain3/index.html, links to static files would look like this (we are assuming to borrow an image from domain1):

<img src="{{url_for('static', filename='domain1/logo01.png')}}">

Pro tip: You can put common libraries from under the static folder and share among domains. For example; you can create the folder /static/bootstrap , put all bootstrap files underneath.

Static HTML files may access bootstrap like:

<script src="/static/bootstrap/js/bootstrap.bundle.min.js" />

Templates may access bootstrap like:

<script src="{{url_for('static', filename='bootstrap/js/bootstrap.bundle.min.js')}}" />

Live usage

As we speak, I am hosting 5 distinct low-traffic websites under a single Heroku dyno using this architecture, and they work just fine. There are many cloud providers where you can host your Python website. I like the simplicity of Heroku and have an article on how to deploy your Python website to Heroku.

You need to be careful about your resources though. Every time you change something and publish to the cloud, you would be publishing all of your websites, entirely!

  • If you only have a small batch of text files and images, you’ll be fine.
  • If you have websites with heavy content, it would be wise to host your content on a separate service (S3, Bucketeer, etc) and make your Python site pull content from that service.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s