Proxy multiple Dash apps running on different ports

I have a handful of Dash SPAs that are running on different ports. I would like to map those applications to URLs on the same domain, e.g. by forwarding incoming GET requests for mydomain.com/app1 => localhost:8050, mydomain.com/app2 => localhost:8051, etc.

I once used nginx for this and as I recall it was pretty straightforward, but I’d prefer not to burden my colleagues with a non-Python dependency (which, as I recall, requires a bit of post-install work to initialize the daemon on the correct port(s), and that sort of work isn’t really in their skill sets).

I tried setting up a rudimentary proxy server in Flask to simply pass contents of the resolved URL back to the client, e.g.:

from flask import Flask
from requests import get
from collections import defaultdict

app = Flask(__name__)
routes = {
    "dashapp1": "http://localhost:8888",
    "dashapp2": "http://localhost:8889",
    "dashapp3": "http://localhost:8890"
}
router = defaultdict(lambda: "http://localhost:8080/404", routes)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def proxy(path):
  route = router[path]
  return get(route).content

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=8080)


This kinda works. But it only fetches the initial “Loading…” message for the proxied urls.
Which makes sense, because Dash applications are asynchronously loaded over a websocket, they’re not just static text documents. But given that’s the case, is it even possible with Flask to bind individual Dash applications to paths on a canonical hostname? (I suspect I might have to use nginx for this part of the application, but I’m hoping maybe someone out there knows of a pure Python solution to this problem.)

I realized shortly after posting this question that I could probably accomplish this by embedding the application in an iframe, and lo and behold this actually works really well! I’m posting my solution here in case anyone else wants to do something similar without modifying HTTP headers or passing IO sockets around.

app.py

from flask import Flask, render_template
from collections import defaultdict

app = Flask(__name__)
routes = {
    "dashapp1": {"template": "index.html", 
                 "url": "http://localhost:8888",
                 "title": "Dash Application #1"},
    "dashapp2": {"template": "index.html",
                 "url": "http://localhost:8889",
                 "title": "Dash Application #2"},
    "dashapp3": {"template": "index.html",
                 "url": "http://localhost:8890",
                 "title": "Dash Application #3"}
}


class RoutingError(dict):
    pass


class Error404(RoutingError):
    def __init__(self):
        super().__init__({"template": "404.html", "url": "", "title": "404 Error"})


router = defaultdict(Error404, routes)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def proxy(path, routes=routes):
  proxied = router[path]
  print(f"Route is: {proxied['url']}")
  return render_template(proxied['template'], iframe=proxied['url'], routes=routes)

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=8080)

templates/layout.html

Decorates the resolved template with site-wide navbar and HTML headers.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="/static/css/normalize.css"/>
    <link rel="stylesheet" type="text/css" href="/static/css/foundation.css"/>

</head>
<body>
    <nav class="top-bar">
            <h1>My Collection of Dash Applications</h1>
    </nav>

    {% block content %} 
    {% endblock content %}

</body>
</html>

templates/index.html

Embeds the resolved url to the requested Dash application.

{% extends 'layout.html' %}
{% block content %}
    <div>
            <h2>Proxied from <a href="{{ iframe }}">{{ iframe }}</a></h2>
    </div>
    <iframe frameborder='0' noresize='noresize' style='position: absolute; background: transparent; width: 100%; height:100%;' src="{{ iframe }}"  frameborder="0"></iframe>
{% endblock content %}

Rendered:

templates/404.html

Not really an error page, just a directory of available Dash applications.

{% extends 'layout.html' %}
{% block content %}
    <ol>
            {% for path, route in routes.items() %}
            <li><a href="./{{ path }}">{{ route['title'] }}</a></li>
            {% endfor %}
    </ol>

{% endblock content %}

Rendered: