Repository: bokeh/bokeh

Issue:
bokeh server compatibility with running behind jupyterhub
Feature Request: Allow bokeh server to run behind JupyterHub (and other reverse proxies)

Right now, bokeh supports being run in a JupyterLab notebook using the appropriate plugin (https://github.com/bokeh/jupyterlab_bokeh).  This plugin is great, and allows for bokeh graphs to be rendered in JupyterLab notebooks.

For JupyterHub, there is an additional level of proxing to get to your JupyterLab environment (and the bokeh server running therein), which is provided by JupyerHub.  This maps URLs like https://jupyterhub.example.com/user/cbanek to the URLs on the JupyterLab environment.

When running bokeh server in a Jupyter notebook, bokeh emits javascript that points the browser to the URL of the bokeh server.  In this case, bokeh doesn't know that the server is behind a reverse proxy (JupyterHub) and therefore emits a URL that cannot be accessed (http://localhost:12345)

There are two components to getting this scenario to work:
1. Proxying URL requests to the correct port on the correct container via URL.
2. Giving bokeh the public URL that uses the proxy to emit into the javascript.  Bokeh can't know the proper URL, as is the case in many reverse proxy situations.

To solve this, I've combined nbserverproxy (https://github.com/jupyterhub/nbserverproxy) to provide a URL that gets me to the bokeh server port given a well formed URL.  This URL is of the form https://jupyterhub.example.com/user/xxx/proxy/port/ .  This means that HTTP requests received on this URL are then sent to the JupyterLab instance on the port provided in the URL.  This allows for a browser given the right URL to forward an HTTP request (or websocket) to any port on JupyterLab.

bokeh doesn't know how to generate these URLs, as bokeh isn't aware of the public URL of the proxy you are connecting to.  To solve this, I will be submitting an associated pull request that allows for the injection of a function to help define this URL, after the port has been bound (and therefore known) but before bokeh emits the URL into the notebook cell.  I am using environment variables to help construct the URL in this injected function, though different cases might need different logic, hence the function.

I've seen only one other person attempting something similar in my research:
https://stackoverflow.com/questions/48625893/using-bokeh-server-in-jupyter-notebook-behind-proxy-jupyterhub


Discussion:

Resolving patch:
diff --git a/bokeh/io/notebook.py b/bokeh/io/notebook.py
--- a/bokeh/io/notebook.py
+++ b/bokeh/io/notebook.py
@@ -434,14 +434,28 @@ def show_app(app, state, notebook_url, port=0):
     ''' Embed a Bokeh serer application in a Jupyter Notebook output cell.
 
     Args:
-        app (Application) :
+        app (Application or callable) :
+            A Bokeh Application to embed inline in a Jupyter notebook.
 
         state (State) :
+            ** Unused **
 
-        notebook_url (str) :
+        notebook_url (str or callable) :
+            The URL of the notebook server that is running the embedded app.
 
-        port (int) : the port the embedded server will listen on. By default
-        the port is 0, which results in the server listening on a random dynamic port.
+            If ``notebook_url`` is a string, the value string is parsed to
+            construct the origin and full server URLs.
+
+            If notebook_url is a callable, it must accept one parameter,
+            which will be the server port, or None. If passed a port,
+            the callable must generate the server URL, otherwise if passed
+            None, it must generate the origin URL for the server.
+
+        port (int) :
+            A port for the embedded server will listen on.
+
+            By default the port is 0, which results in the server listening
+            on a random dynamic port.
 
     Returns:
         None
@@ -454,17 +468,28 @@ def show_app(app, state, notebook_url, port=0):
 
     loop = IOLoop.current()
 
-    origin = _origin_url(notebook_url)
+    if callable(notebook_url):
+        origin = notebook_url(None)
+    else:
+        origin = _origin_url(notebook_url)
+
     server = Server({"/": app}, io_loop=loop, port=port,  allow_websocket_origin=[origin])
 
     server_id = uuid4().hex
     curstate().uuid_to_server[server_id] = server
 
     server.start()
-    url = _server_url(notebook_url, server.port)
+
+    if callable(notebook_url):
+        url = notebook_url(server.port)
+    else:
+        url = _server_url(notebook_url, server.port)
+
+    logging.debug("Server URL is %s" % url)
+    logging.debug("Origin URL is %s" % origin)
 
     from ..embed import server_document
-    script = server_document(url)
+    script = server_document(url, resources=None)
 
     publish_display_data({
         HTML_MIME_TYPE: script,
diff --git a/bokeh/io/showing.py b/bokeh/io/showing.py
--- a/bokeh/io/showing.py
+++ b/bokeh/io/showing.py
@@ -99,6 +99,11 @@ def show(obj, browser=None, new="tab", notebook_handle=False, notebook_url="loca
             standard checks, so that applications will display regardless of
             the current notebook location, however a warning will appear.
 
+            notebook_url can also be a function that takes one int for the
+            bound server port.  If the port is provided, the function needs
+            to generate the full public URL to the bokeh server.  If None
+            is passed, the function is to generate the origin URL.
+
 
     Some parameters are only useful when certain output modes are active:
 


Test patch: