Search code examples
jupyter-notebookjupyterbokehjupyterhub

Using bokeh server in jupyter notebook behind proxy / jupyterhub


I want to develop bokeh apps on a jupyter notebook instance that runs behind jupyterhub (AKA an authenticating proxy). I would like to have interactive bokeh apps calling back to the notebook kernel. I don't want to use the notebook widgets etc because I want to be able to export the notebook as a python file and have something I can serve with bokeh server.

The following code in my notebook gives an empty output with no errors:

from bokeh.layouts import row
from bokeh.models.widgets import Button
from bokeh.io import show, output_notebook
from bokeh.application.handlers import FunctionHandler
from bokeh.application import Application
output_notebook()
# Create the Document Application
def modify_doc(doc):
    layout = row(Button(label="Hello,"),Button(label="world!"))
    doc.add_root(layout)

handler = FunctionHandler(modify_doc)
app = Application(handler)

# Output = BokehJS 0.12.10 successfully loaded.


# New cell
show(app, notebook_url="my-jupyterhub-url.com:80")

# Output = "empty" cell

Inspecting the cell a script tag has been added:

<script src="http://my-jupyterhub-url.com:46249/autoload.js?bokeh-autoload-element=f8fa3bd0-9caf-473d-87a5-6c7b9680648b&amp;bokeh-absolute-url=http://my-jupyterhub-url.com:46249" id="f8fa3bd0-9caf-473d-87a5-6c7b9680648b" data-bokeh-model-id="" data-bokeh-doc-id=""></script>

This will not work because port 46249 isn't open on the jupyterhub proxy. Also the path that routes to my jupyter instance is my-jupyterhub-url.com/user/my-username/ so my-jupyterhub-url.com/autoload.js wouldn't route anywhere.

This feels like it could be a common requirement but a search hasn't revealed a solution to be yet.

Any ideas?


Solution

  • So I've found a solution that I'm not happy about but works.. just about.

    First install nbserverproxy on your Jupyter instance. This allows you to proxy through JupyterHub (where you are authenticated) onto arbitrary ports on your Jupyter machine/container. I installed by opening a terminal from the Jupyter web front end and typing:

    pip install git+https://github.com/jupyterhub/nbserverproxy --user
    jupyter serverextension enable --py nbserverproxy --user
    

    Then restart your server. For my install of JupyterHub this was control panel -> stop my server wait then start my server.

    Finally I monkey patched the Ipython.display.publish_display_data (since the source code revealed that bokeh used this when calling show) in the notebook like so.

    from unittest.mock import patch
    
    from IPython.display import publish_display_data
    orig = publish_display_data
    
    import re
    def proxy_replacer(display_data):
        for key, item in display_data.items():
            if isinstance(item, str):
                item= re.sub(r'(/user/tam203)/?:([0-9]+)', r'\1/proxy/\2', item)
                item = re.sub(r'http:' , 'https:', item)
                display_data[key] = item 
        return display_data
    
    
    def mock(data, metadata=None, source=None):
        data = proxy_replacer(data) if data else data 
        metadata = proxy_replacer(metadata) if metadata else metadata
        return orig(data, metadata=metadata, source=source)
    
    
    patcher = patch('IPython.display.publish_display_data', new=mock)
    patcher.start()
    

    With that all done I was then able to run the following an see a nice dynamically updating plot.

    import random
    from bokeh.io import output_notebook 
    output_notebook()
    from bokeh.io import show
    from bokeh.server.server import Server
    from bokeh.application import Application
    from bokeh.application.handlers.function import FunctionHandler
    from bokeh.plotting import figure, ColumnDataSource
    
    
    def make_document(doc):
        source = ColumnDataSource({'x': [], 'y': [], 'color': []})
    
        def update():
            new = {'x': [random.random()],
                   'y': [random.random()],
                   'color': [random.choice(['red', 'blue', 'green'])]}
            source.stream(new)
    
        doc.add_periodic_callback(update, 100)
    
        fig = figure(title='Streaming Circle Plot!', sizing_mode='scale_width',
                     x_range=[0, 1], y_range=[0, 1])
        fig.circle(source=source, x='x', y='y', color='color', size=10)
    
        doc.title = "Now with live updating!"
        doc.add_root(fig)
    
    app =  Application(FunctionHandler(make_document))
    show(app, notebook_url="<my-domain>.co.uk/user/tam203/")
    

    So while I'm happy to have found a work around it doesn't really feel like a solution. I think a smallish change in bokeh could solve this (something like a url template string where you can specify the path and the port).