instructions on how to deploy a dashboard with gunicorn

Example

An example dashboard can be found at github.com/oegedijk/dash_oop_demo and has been deployed to https://dash-oop-demo.herokuapp.com/

First define the DashFigureFactory and DashComponents in dashboard.components.py. In this case I named them CovidPlots and CovidDashboard.

Then build the dashboard and save to dashboard.yaml:

build_dashboard.py:

from dashboard_components import CovidPlots, CovidDashboard
from dash_bootstrap_components.themes import FLATLY
from dash_oop_components import DashApp

plot_factory = CovidPlots(datafile="covid.csv")
dashboard_component = CovidDashboard(plot_factory)
db = DashApp(dashboard_component, querystrings=True, bootstrap=FLATLY)
db.to_yaml("dashboard.yaml")

Then define a dashboard.py that builds the dashboard from config, and exposes the Flask server:

dashboard.py

from dash_oop_components import DashApp

db = DashApp.from_yaml("dashboard.yaml")
app = db.app.server

And start the gunicorn server:

$ gunicorn --preload -b localhost:8050 dashboard:app

Alternatively you can ofcourse simply use the dashapp CLI for starting the dashboard outside production environments:

$ dashapp dashboard.yaml

Automatically reloading dashboard whenever config changes

You can also automatically restart the gunicorn server whenever there is a change to dashboard.yaml, by using watchdog. Install with pip install watchdog[watchmedo]. Start the gunicorn server while saving it's pid:

$ gunicorn --pid gunicorn.pid --preload -b localhost:8050 dashboard:app

And the start a watchmedo script that runs kill -HUP on the gunicorn server in order to force a restart whenever it detects a change to dashboard.yaml:

$ watchmedo shell-command -p "./dashboard.yaml" -c 'kill -HUP $(cat gunicorn.pid)'

Loading with pickled DashFigureFactory

In some cases you might be doing some expensive calculations inside your DashFigureFactory that you do not want to run everytime you restart a dashboard as it would break the gunicorn timeout window. The solution is to build the plot_factory, indicate the filepath of the dumped pickle file, dump it, and then load it from the pickle file when you start the dashboard, by passing try_pickles=True:

build_dashboard.py:

from dashboard_components import CovidPlots, CovidDashboard
from dash_bootstrap_components.themes import FLATLY
from dash_oop_components import DashApp

plot_factory = CovidPlots(datafile="covid.csv", filepath="plot_factory.pkl")
plot_factory.dump() # stores to plot_factory.pkl

dashboard_component = CovidDashboard(plot_factory)

db = DashApp(dashboard_component, bootstrap=FLATLY)
db.to_yaml("dashboard.yaml")

When you pass try_pickles=True if the filepath cannot be found, then the DashPlotFactory will get rebuilt from config. If you pass force_pickles=True, then DashApp will raise an exception instead:

dashboard.py:

from dash_oop_components import DashFigureFactory, DashComponent, DashApp

db = DashApp.from_yaml("dashboard.yaml", try_pickles=True)
app = db.app.server

Fully automated build and redeploy cycle

We can also you watchmedo to rebuild the plot_factory whenever there is a change to plot_factory.yaml or covid.csv, and then rebuild the dashboard whenever there is a change to plot_factory.pkl, dashboard_component.yaml or dashboard.yaml:

build_plot_factory.py:

from dash_oop_component import DashFigureFactory

plot_factory = DashFigureFactory.from_yaml("plot_factory.yaml", filepath="plot_factory.pkl")
plot_factory.dump("")

build_dashboard.py:

from dashboard_components import CovidPlots, CovidDashboard
from dash_bootstrap_components.themes import FLATLY
from dash_oop_components import DashApp

plot_factory = DashFigureFactory.from_file("plot_factory.pkl")
dashboard_component = CovidDashboard(plot_factory)
db = DashApp(dashboard_component, bootstrap=FLATLY)

db.to_yaml("dashboard.yaml")

dashboard.py:

from dash_oop_components import DashFigureFactory, DashComponent, DashApp

db = DashApp.from_yaml("dashboard.yaml", try_pickles=True)
app = db.app.server

Now start the gunicorn server with:

$ gunicorn --pid gunicorn.pid --preload -b localhost:8050 dashboard:app

Now run the build_plot_factory.py script everytime you detect a change in either covid.csv or plot_factory.yaml:

$ watchmedo shell-command -p "./covid.csv;./plot_factory.yaml" -c 'python build_plot_factory.py'

Now run the build_dashboard.py script everytime you detect a change in either plot_factory.pkl or plot_factory.yaml:

$ watchmedo shell-command -p "./plot_factory.pkl;./plot_factory.yaml" -c 'python build_dashboard.py'

And restart the gunicorn server everytime you detect a change to plot_factory.pkl, or dashboard.yaml:

$ watchmedo shell-command -p "./plot_factory.pkl;./dashboard.yaml" -c 'kill -HUP $(cat gunicorn.pid)'

Or in a single shell script:

start_server.sh:

trap "kill 0" EXIT  # ensures that all four process are killed upon exit

source venv/bin/activate # activate virtual environment first

gunicorn --pid gunicorn.pid dashboard:app &
watchmedo shell-command -p "./covid.csv;./plot_factory.yaml" -c 'python build_plot_factory.py' &
watchmedo shell-command -p "./plot_factory.pkl;./plot_factory.yaml" -c 'python build_dashboard.py' &
watchmedo shell-command -p "./plot_factory.pkl;./dashboard.yaml" -c 'kill -HUP $(cat gunicorn.pid)'


wait # wait till user hits ctrl-c to exit and kill all three processes