How to Deploy Plotly's Dash using Shinyproxy

In a previous post I established that I could easily deploy a 'Hello World' flask.py web application using Shinyproxy. Therefore, I thought it would be straightforward to deploy a Dash app which is built on top of flask.py. However, it proved to be a little more difficult than that. This blog post runs through the errors and eventual solution to deploying a Dash app on Shinyproxy.

The Issue

Dash apps, like most web apps, load additional static resources, but upon deploying on Shinyproxy these aren't being delivered to the client (browser) as the URL path is wrong. This results in the all too familiar 404 Not Found. Disclaimer: I haven't programmed in Java or SpringBoot (yet!). However, as far as I can see from looking at Chrome dev tools there are two ways Shinyproxy delivers static files to the client. I'm taking a look first at how it works with a Shiny app.

  1. Webjars - I can see that the main template of shinyproxy loads the bootstrap CSS which also gets utilised by each Shiny app.

  2. Load from the container - Additional static content for shiny apps is requested by using the container name, in this case peaceful_jepsen e.g.:

Request URL:http://<my-ip-address>/peaceful_jepsen/dt-core-1.10.12/css/jquery.dataTables.min.css  

Now let's move on to it not working with Dash...

Deploying a Dash App on Shinyproxy

Using react.min.js as an example, I'm going to contrast how the app fetches static content on my local machine in a container vs Shinyproxy on a remote server. I'll do this by inspecting the path for the GET requests using Chrome dev tools. Firstly, what is the default behaviour of Dash.

By default, dash serves the JavaScript and CSS resources from the online CDNs. Source: Dash Docs

Dash Container on my Local Machine local-dash-cdn-get

All resources loaded and the application displayed correctly.

Dash Container on Shinyproxy shinyproxy-dash-cdn-get

On Shinyproxy react.min.js gets loaded however _dash-layout and _dash-dependencies fail to GET the necessary resources from the Request URL: http://<my-ip-address>/_dash-layout. As we know from the Shiny app, the container name should be in there e.g. Request URL: http://<my-ip-address>/<container_name>/_dash-layout. This is how Dash is supposed to behave, looking at the source code we can see a configurable routes_pathname_prefix (I'll come back to this later in the post). So a possible solution is to get the Dash app to obtain the container name from which it is running.

The Dash Docs have the following lines of code to serve content locally.

app.css.config.serve_locally = True  
app.scripts.config.serve_locally = True  

Let's see how that changes the behaviour.

Dash Container on my Local Machine local-dash-serve-local-get Since all static content is now requested from Dash this is probably going to prevent all content from being loaded on Shinyproxy.

Dash Container on Shinyproxy shinyproxy-dash-serve-local-get Yup! A whole lot of red 404s.

Essentially it doesn't matter if you configure Dash to serve static files locally, the underlying issue is the relative URL route prefix. We know from the Dash source code how we could prefix the URL route. However, Shinyproxy is already doing this for Shiny apps, what's different for the requests from Dash?

I started scouring the Shinyproxy Java code base. I found the construction of the containerPath in Shinyproxy's AppController.java. But since I am not a Java developer I decided that this would probably not be the best allocation of my efforts. However, what the containerPath does show is the construction of the path begins with '/'.

Another approach I thought of was obtaining the container name from within the Dash application and appending that to the GET URL requests initiated Dash. Alas, Docker doesn't allow that. You can access the ContainerID but not the ContainerName. There is a 3 year old open issue regarding this. I then started looking for an easy way to pass into the container the containerName but I couldn't find a non-hacky way to do that either.

Sidenote - I was wondering what the logic was on the container name generation e.g. adorin_raman. If you look at the docker source code you will find some go-lang code called names-generator.go. This contains two lists, a list of adjectives and a list of "notable scientists and hackers". The function that randomly combines these into a container name formatted as "adjective_surname" has one funny exception:

if name == "boring_wozniak" /* Steve Wozniak is not boring */ {  
    goto begin
}

Two other approaches that weren't very appealing would have been managing the resources as webjars or an Nginx redirect.

The Final Solution

I posted a question to the shinyproxy forum, Frederick one of the main authors responded with:

Then that means the href being used was /_dash-layout and not _dash-layout. Do you control the hrefs? If so, removing the slash may solve your issue. If not, it becomes trickier… you could use javascript and extract the correct URL from window.location.

If we dig into the dash.py code We can see the construction of the URL paths for _dash-layout _dash-dependencies occurs on lines 73-80:

add_url(  
    '{}_dash-layout'.format(self.config['routes_pathname_prefix']),
    self.serve_layout)

add_url(  
    '{}_dash-dependencies'.format(self.config['routes_pathname_prefix']), self.dependencies)

The next question is, where is routes_pathname_prefix defined? On lines 46-51 routes_pathname_prefix is mapped to url_base_pathname:

self.url_base_pathname = url_base_pathname  
self.config = _AttributeDict({  
    'suppress_callback_exceptions': False,
    'routes_pathname_prefix': url_base_pathname,
    'requests_pathname_prefix': url_base_pathname
})

Which begs the question, where is url url_base_pathname defined... on lines 20-28:

class Dash(object):  
    def __init__(
        self,
        name=None,
        server=None,
        static_folder=None,
        url_base_pathname='/',
        **kwargs
    ):

As Frederick said, the request should not have the '/' prefix in the GET request. Unsurprisingly, having '//' in your request paths is not good practice (see this SO answer and this one) as it can cause problems depending on how requests are handled. The software layers of Shinyproxy are reasonably complex with requests being handled my multiple technologies. The best solution after this trouble-shooting exercise is to use the following code in your Dash app.

# In order to work on shinyproxy (and perhaps other middleware)
app.config.supress_callback_exceptions = True  
app.config.update({  
    # remove the default of '/'
    'routes_pathname_prefix': '',

    # remove the default of '/'
    'requests_pathname_prefix': ''
})