5

I have a Flask app where I have embedded a Bokeh server graph and I am not being able to have them both working on Heroku. I am trying to deploy on Heorku and I can either start the Bokeh app or the Flask app from the Procfile, but not both at the same time. Consequently, either the content served with Flask will show, or the Bokeh graph.

When I deploy with the following line in Procfile, the Bokeh content shows up on the webpage, but not nothing from Flask:

web: bokeh serve --port=$PORT --host=bokehapp.herokuapp.com --host=* --address=0.0.0.0 --use-xheaders bokeh_script.py

If I deploy with the following, I only get the Flask content, not the Bokeh graph:

web: gunicorn app:app

In the second case, I am starting Bokeh inside the app.py Flask script using a subprocess:

bokeh_process = subprocess.Popen(
    ['bokeh', 'serve','--allow-websocket-origin=bokehapp.herokuapp.com','--log-level=debug','standard_way_with_curdoc.py'], stdout=subprocess.PIPE)

The Heroku logs don't show any errors.

I also tried a third alternative:

web: bokeh serve --port=$PORT --host=bokehapp.herokuapp.com --host=* --address=0.0.0.0 --use-xheaders bokeh_script.py
web: gunicorn app:app

And that shows Flask content only. It seems only second worker is being considered.

So, my question is how modify the Procfile to consider both processes? Or maybe I am approaching this wrong all together? Any clue you can give would be appreciated.

multigoodverse
  • 7,638
  • 19
  • 64
  • 106

1 Answers1

7

Each Heroku dyno gets allocated a single public network port, which you can get from the $PORT variable, pre-assigned by Heroku before launching your app. The unfortunate side effect of this is that you can have only one public web server running in a dyno.

I think the first thing that you need to do is route all requests to your application through your Flask server. For example, you can add a /bokeh/<path:path> route to your Flask app that forwards all requests to the bokeh server using requests, then sends the response back to the client. With this change, you now have a single public web server, and the bokeh server can run as a background service without public access.

So now you can deploy your Flask app to Heroku, and have it receive both its own requests and the requests for the bokeh server. The next step is to figure out where to deploy the bokeh server.

The proper way to do this is to deploy bokeh on a separate dyno. The Flask dyno will know how to forward requests to bokeh because you will have a configuration item with the bokeh server URL.

If you want to have everything hosted on a single dyno I think you can as well, though I have never tried this myself and can't confirm it is viable. The all in one configuration is less ideal and not what Heroku recommends, but according to the dyno networking documentation it appears you can privately listen on any network ports besides $PORT. Those are not exposed publicly, but the doc seems to suggest the processes running inside a dyno can communicate through private ports. So for example, you can start the Flask app on $PORT, and the bokeh server on $PORT + 1, and have Flask internally forward all the bokeh requests to $PORT + 1.

Miguel Grinberg
  • 65,299
  • 14
  • 133
  • 152
  • Thank you Miguel. I created this route: `@app.route('/myplot') def myplot(): r=requests.get("http://localhost:5006",stream=True) return Response(stream_with_context(r.iter_content()), content_type = r.headers['content-type'])` – multigoodverse Sep 26 '16 at 12:24
  • That doesn't give an error on the server part but it renders a blank webpage http:bokehapp.heroku.com/myplot. Inspecting the page gives a `WebSocket connection to 'ws://localhost:5006/standard_way_with_curdoc/ws?bokeh-protocol-version=1.0&bokeh-session-id=ArJfCQQCIZaD5EM3VgBGRqdVzlxQDazwxEhLGV6byDTD' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED` It's running fine on localhost. – multigoodverse Sep 26 '16 at 12:24
  • I did not know bokeh used websocket routes. That complicates things. Maybe you should consider doing the reverse, which is to mount your Flask app inside the bokeh server. – Miguel Grinberg Sep 26 '16 at 14:45
  • Or maybe you can get by with just hosting Flask and bokeh servers each on its own dyno? – Miguel Grinberg Sep 26 '16 at 14:48
  • I have yet to try the option involving another dyno. I fond the reverse a smart option and tried it, but the index page redirects to the bokeh graph path. That's because of this function inside Flask: `@app.route("/") def index(): session=pull_session(app_path="/standard_way_with_curdoc") bokeh_script=autoload_server(None,app_path="/standard_way_with_curdoc",session_id=session.id) return render_template("index.html", bokeh_script=bokeh_script)` – multigoodverse Sep 27 '16 at 14:29
  • I just got two hobby dynos one for Flask and one for Bokeh, but I keep getting a blank page. Inspecting it brings `GET http://localhost:5006/standard_way_with_curdoc/autoload.js?bokeh-autoload-e…c36fb4cf3ecf&bokeh-session-id=JMT3iaPwqbTye09mKC9zTXDpTKJ9kmuDKYJVINHShEq2 net::ERR_CONNECTION_REFUSED` – multigoodverse Sep 27 '16 at 16:11
  • @adi what's a `localhost` URL doing in this? You have to configure these two apps so that they talk to each other on their public Heroku addresses. – Miguel Grinberg Sep 27 '16 at 16:24
  • I see. Bokeh server which is started through the subprocess starts at localhost:5006 by default. I just tried to specify a host and a port, but I get in the logs that bokeh server has not started. This is what I tried: `bokeh_process = subprocess.Popen( ['bokeh', 'serve','--host=bokehapp.herokuapp.com','--address=0.0.0.0','--allow-websocket-origin=bokehapp.herokuapp.com','--log-level=debug','standard_way_with_curdoc.py'], stdout=subprocess.PIPE)` And here are all the files if you want to take a look: https://github.com/adi700/mybokehappname – multigoodverse Sep 27 '16 at 17:21
  • Sorry, I lost track of what you are trying. The repo you linked does not have a standalone bokeh app configured for Heroku. I thought you were going to start bokeh as a standalone dyno, so why do you need subprocess? That does not work as I explained in my answer, there is only one public port in a dyno that you can use, the one that Heroku gives you in the $PORT environment variable. – Miguel Grinberg Sep 27 '16 at 17:45
  • Sorry, I lost track too for a while. I tried option. There's more than 60 versions deployed already. Lastly, I just added the bokeh starter in Procfile below the flask starter. The logs say bokeh server hasn't started. I also tried the current configuration at github.com/adi700/mybokehappname with two hobby dynos, but still nothing. – multigoodverse Sep 28 '16 at 15:34
  • I don't understand. In your question above you say that you can deploy the bokeh server standalone. What changed now? – Miguel Grinberg Sep 28 '16 at 15:43
  • Yes, I was able to deploy bokeh server by having only `web: bokeh serve --port=$PORT --host=bokehapp.herokuapp.com --host=* --address=0.0.0.0 --use-xheaders bokeh_script.py` in the procfile. If I add the flask line `web: gunicorn app:app` above that line or below, it gives the error `bokeh server hasn't started`. Are you saying I need to deploy bokeh and flask in separate Heroku apps? – multigoodverse Sep 28 '16 at 15:57
  • Yes, that was my main suggestion. Two separate apps, on separate Heroku dynos. – Miguel Grinberg Sep 28 '16 at 16:40
  • Sorry, I now understand. I just deployed Bokeh and Flask in two separate apps, and I am now able to access the bokeh graph from the flask app. I have yet to figure out how to embed the bokeh graph inside the HTML template though. Currently I am only showing the bare bokeh graph through Response. My function `@app.route('/myplot') def myplot(): r=requests.get("http://localhost:5006",stream=True) return Response(stream_with_context(r.iter_content()), content_type = r.headers['content-type'])` Had no success with `render_template('index.html', bokeh_script=stream_with_context(r.iter_content())` – multigoodverse Sep 30 '16 at 13:43
  • The first option is the correct one. You need to add a reference to `/myplot` somewhere in your html, so that when Flask returns the html, the browser then goes and grabs the graph. – Miguel Grinberg Sep 30 '16 at 15:58