(nginx+gunicorn+flask+dash) dash Firewall problem

I set up a website with flask and dash. I configured gunicorn and nginx, and deployed the app on a virtual server. I registered the domain, and all was working fine, with the website accessible from the Internet.

Then, our network administrator updated the firewall, and now flask works, but not dash.
All still works correctly if I access the website from 127.0.0.1 from a locally running browser.

If I access the website from the Internet, I get an empty page instead of the app, when accessing its routes_pathname_prefix, and on the console of the browser I get the following error:

dash_renderer.v2_10_2m1730126951.min.js:2 Error: Minified Redux error #14; visit https://redux.js.org/Errors?code=14 for the full message or use the non-minified dev environment for full errors. 
    at dash_renderer.v2_10_2m1730126951.min.js:2:106040
    at dash_renderer.v2_10_2m1730126951.min.js:2:194804
    at dash_renderer.v2_10_2m1730126951.min.js:2:194659
    at p (dash_renderer.v2_10_2m1730126951.min.js:2:67849)
    at dash_renderer.v2_10_2m1730126951.min.js:2:69045
    at Ds (dash_renderer.v2_10_2m1730126951.min.js:2:231090)
    at commitHookEffectListMount (react-dom@16.v2_10_2m1730126952.14.0.js:19866:28)
    at commitPassiveHookEffects (react-dom@16.v2_10_2m1730126952.14.0.js:19904:13)
    at HTMLUnknownElement.callCallback (react-dom@16.v2_10_2m1730126952.14.0.js:182:16)
    at Object.invokeGuardedCallbackDev (react-dom@16.v2_10_2m1730126952.14.0.js:231:18)

My question is: how shall the firewall be configured to let the website work as before?

Our admin would prefer that all the requests are routed via 443, as it is now the case to access locally port 5000 via nginx, where flask is serving its content via gunicorn, but I am not sure if this is possible with dash.

Thank you for your suggestions!

Hello @Blast,

Welcome to the community!

Did you try importing your flask app to your dash app as the server and then running your dash app server from gunicorn?

I am not sure if I did already how you are suggesting. This is how I did:
https://hackersandslackers.com/plotly-dash-with-flask/

All this made my head spin a bit, but it worked quite nicely, until the firewall configuration was changed.

Thank you!

Do you have the flask app as a separate app?

Here is my understanding of the configuration:
Gunicorn serves the app, which is run as a flask app:

app = Flask(__name__, static_url_path='/static')

The Flask app (listening on port 5000) is then passed to the init_app function, as a server for Dash:

with app.app_context():
            # Import parts of our core Flask app
            from . import routes
            from .assets import compile_static_assets
    
            # Import Dash application
            from .dashboard import init_dashboard
            app = init_dashboard(app,"/dashapp/")

And here the Flask server is associated to the Dash app:

def init_dashboard(server,url):
    """Create a Plotly Dash dashboard."""
    dash_app = dash.Dash(
        server=server,
        serve_locally=True,
        routes_pathname_prefix=url,
        external_stylesheets=[
            "/static/dist/css/styles.css",
            "https://fonts.googleapis.com/css?family=Lato",
        ]
    )
    
    # Both app context and a request context to use url_for() in the Jinja2 templates are needed
    with server.app_context(), server.test_request_context():
        html_body = render_template_string(html_layout, dash='yes')
        comments_to_replace = ("metas", "title", "favicon", "css", "app_entry", "config", "scripts", "renderer")
        for comment in comments_to_replace:
            html_body = html_body.replace(f"<!-- {comment} -->", "{%" + comment + "%}")
        dash_app.index_string = html_body

    # Create Layout
    dash_app.layout = serve_layout
    init_callbacks(dash_app)
    return dash_app.server

So my understanding is that Flask calls dash when the routes_pathname_prefix is called by a Flask route, generating the corresponding URL and triggering the serve_layout function (not shown)

Flask generates a page with jinja2, that is also used in Dash (code not shown) to embed the app into the HTML/Javascript code (again, according to my understanding).

I hope this answers to your question, and is useful to troubleshoot the Firewall problem.

Thank you!

Gunicorn will listen by default on port :8000.

The interesting thing was your statement of flask working but Dash not… Anyways, that demo doesnt show how to actually run the gunicorn command.

What does your gunicorn command look like? What is your nginx routing file?

2 Likes

This is my flask.service:

[Unit]
Description=Gunicorn instance to serve Flask
After=network.target
[Service]
User=root
Group=www-data
WorkingDirectory=/home/XXXXXX/plotly_flask_app
Environment="PATH=/home/XXXXXX/plotly_flask_app/venv/bin"
ExecStart=/home/XXXXXX/plotly_flask_app/venv/bin/gunicorn --bind 127.0.0.1:5000 run:app
[Install]
WantedBy=multi-user.target

And here is the nginx configuration:

server {
    listen 443 ssl;
    server_name 192.X.X.X

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    ssl_certificate /etc/ssl/certs/XXXXX.cer;
    ssl_certificate_key /etc/ssl/private/XXXXX.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://127.0.0.1:5000;  # Assuming Flask runs on port 5000
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

# Redirect HTTP traffic to HTTPS
server {
    listen 80;
    server_name your_domain_or_ip;

    location / {
        return 301 https://$host$request_uri;
    }
}

And then the app starts with run.py:

from plotly_flask_app import init_app

app = init_app()

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")

So in this case, since the server is Flask, gunicorn listen on port :5000
Flask works, as I implemented a user authentication which is accessible and OK. But as I Invoke a route toward the Dash app, I get the above error in the browser, and a blank section of the page, instead of getting served the layout. I can directly use the layout URL as I can see it in the Flask debug toolbar, and I get the HTML code back, but it is as if Dash was not processing it.

All works from 127.0.0.1 and of course from 127.0.0.1:5000. It used to work also when the server was called from the internet, but not anymore after a Firewall upgrade. So my question is “how can the Firewall be configured to get things back up to work”?

Let me know if any other technical information can be useful.

Thank you!

Can you post your plotly_flask_app?

It seems that you arent loading your Dash app onto the server with the code that you are running.

I use Flask, Dash, Gunicorn and Nginx btw.

1 Like

Here is the Flask part of the app:

from flask import Flask, request,render_template
from flask_restful import Resource, Api
from flask_sqlalchemy import SQLAlchemy
from flask_debugtoolbar import DebugToolbarExtension
from flask_bcrypt import Bcrypt
from flask_login.login_manager import LoginManager
import sys

from flask_assets import Environment
import flask_login


def init_app():
    app = Flask(__name__,  static_url_path='/static')
    app.config.from_object("config.Config")
    
    assets = Environment()
    assets.init_app(app)
    
    with app.app_context():
            # Import parts of our core Flask app
            from . import routes
            from .assets import compile_static_assets
    
            # Import Dash application
            from .dashboard import init_dashboard
            app = init_dashboard(app,"/dashapp/")
    
            # Compile static assets
            compile_static_assets(assets)
    
    app.debug = False
    #api = Api(app)
    port = 5000
    # set a 'SECRET_KEY' to enable the Flask session cookies
    app.config['SECRET_KEY'] = "XXXXXXXX"
    
    toolbar = DebugToolbarExtension(app)
    
    if sys.argv.__len__() > 1:
        port = sys.argv[1]
    print("Api running on port : {} ".format(port))
   
    return(app)

And here is the Dash part (init_dashboard):


def init_dashboard(server,url):
    """Create a Plotly Dash dashboard."""
    dash_app = dash.Dash(
        server=server,
        serve_locally=True,
        routes_pathname_prefix=url,
        external_stylesheets=[
            "/static/dist/css/styles.css",
            "https://fonts.googleapis.com/css?family=Lato",
        ]
    )
    
    # Enable development tools
    dash_app.enable_dev_tools(
        dev_tools_ui=False,
        dev_tools_serve_dev_bundles=False,
    )

    # Both app context and a request context to use url_for() in the Jinja2 templates are needed
    with server.app_context(), server.test_request_context():
        html_body = render_template_string(html_layout, dash='yes')

        comments_to_replace = ("metas", "title", "favicon", "css", "app_entry", "config", "scripts", "renderer")
        for comment in comments_to_replace:
            html_body = html_body.replace(f"<!-- {comment} -->", "{%" + comment + "%}")

        dash_app.index_string = html_body

    # Create Layout
    dash_app.layout = serve_layout
    init_callbacks(dash_app)
    return dash_app.server

So in the end I am too using Flask, Dash, Gunicorn and Nginx. It is just that I do not know which ports are needed by Dash, as something is definitely being blocked at firewall level.

You need to be doing this:

from plotly_flask_app import init_app
from plotly_dash_app import init_dashboard

server = init_app()
app = init_dashboard(server, url)

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")

And your ports should be fine. You were never taking the app and attaching dash to it.