Request for nginx config for Flask + Bokeh server

hi @Bryan, I am able to configure nginx + bokeh but i need to bring in Flask in between as i have multiple Flask apps with some UI styling that i need to carry on to bokeh app as well. I am able to have my flask app render my bokeh app (i.e. if I go to my.domain.name:5000 i will see my bokeh app with headers/footers correctly added by index.html from flask app) but when i try to config this thru nginx i get all sorts of issues ranging from bad gateway to blank pages.

The bokeh doc page shows a few sample nginx configs but only for nginx + bokeh. It would be really helpful if some simple examples are shown for nginx + flask where flask is rendering bokeh server apps. I went thru this discussion in this forum but it deals with deprecated autoload_server to start with and the subsequent link to stackoverflow had too many moving parts (like putting an upstream server etc.). I tried quite a few things from those solution but still no luck. Since i tried so many combinations it’s hard to put down a single error here.

Any chance of getting a simple config sample that works? Any links to other forums are also welcome.

I am afraid I don’t actually have any more experience with nginx than what is there the in the docs. (literally—I have never really used nginx before or since writing those) I am very far from being any sort of expert about nginx. Someone else here might be able to chime in, but in general I think you might find better expertise on an Nginx-specific help forum.

FWIW autoload_server can generally be replaced with server_document (or if you are customizing sessions, server_session, but that is less common)

@Gopi_M

The following is a very basic example to illustrate using nginx with a Flask app under gunicorn control which embeds a bokeh server document.

DISCLAIMER This example uses an HTTP server only (no HTTPS protocols, SSL keys/certificates, etc.) that I was using quite a while ago to get a handle on integration. I now do everything via Heroku and its nginx buildpack to host online and have HTTPS, etc.

With that background, the relevant pieces are the nginx configuration file (nginx.conf), the gunicorn configuration file (gunicorn_config.py), and the flask app (flask_gunicorn_embed.py). The flask app is a very minor modification of the official bokeh flask+gunicorn embed example to leverage the server and port variables I have defined, but otherwise functionally the same. I have not included the supporting theme and templates required to run the example for brevity.

The server is invoked via …

gunicorn --config gunicorn_config.py flask_gunicorn_embed:app

And for the server on the local network, a client accesses via the following in the browser search bar. Change the nginx server parameters as required for your network topology.

http://192.168.0.135:8080/

nginx.conf

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8080;
        server_name  192.168.0.135;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        #location / {
        #    root   html;
        #    index  index.html index.htm;
        #}

        location / {
            proxy_pass http://127.0.0.1:5100;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_http_version 1.1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host:$server_port;
            proxy_buffering off;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
    include servers/*;
}

gunicorn_config.py

import multiprocessing
import urllib.parse

bind = "127.0.0.1:5100"
workers = multiprocessing.cpu_count() * 2 + 1

pr = urllib.parse.urlparse('//'+bind)
APP_SERVER_ADDR = pr.hostname
APP_SERVER_PORT = pr.port

NGINX_SERVER_ADDR = "192.168.0.135"
NGINX_SERVER_PORT = 8080

flask_gunicorn_embed.py

try:
    import asyncio
except ImportError:
    raise RuntimeError("This example requries Python3 / asyncio")

from threading import Thread

from flask import Flask, render_template
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop

from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
from bokeh.embed import server_document
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
from bokeh.server.server import BaseServer
from bokeh.server.tornado import BokehTornado
from bokeh.server.util import bind_sockets
from bokeh.themes import Theme

from gunicorn_config import APP_SERVER_ADDR, APP_SERVER_PORT
from gunicorn_config import NGINX_SERVER_ADDR, NGINX_SERVER_PORT


if __name__ == '__main__':
    print('This script is intended to be run with gunicorn. e.g.')
    print()
    print('    gunicorn -w 4 flask_gunicorn_embed:app')
    print()
    print('will start the app on four processes')
    import sys
    sys.exit()


app = Flask(__name__)

def bkapp(doc):
    df = sea_surface_temperature.copy()
    source = ColumnDataSource(data=df)

    plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',
                  title="Sea Surface Temperature at 43.18, -70.43")
    plot.line('time', 'temperature', source=source)

    def callback(attr, old, new):
        if new == 0:
            data = df
        else:
            data = df.rolling('{0}D'.format(new)).mean()
        source.data = ColumnDataSource.from_df(data)

    slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
    slider.on_change('value', callback)

    doc.add_root(column(slider, plot))

    doc.theme = Theme(filename="theme.yaml")

# can't use shortcuts here, since we are passing to low level BokehTornado
bkapp = Application(FunctionHandler(bkapp))

# This is so that if this app is run using something like "gunicorn -w 4" then
# each process will listen on its own port
sockets, port = bind_sockets(APP_SERVER_ADDR, 0)

@app.route('/', methods=['GET'])
def bkapp_page():
    script = server_document('http://%s:%d/bkapp' % (APP_SERVER_ADDR,port))
    return render_template("embed.html", script=script, template="Flask")

def bk_worker():
    asyncio.set_event_loop(asyncio.new_event_loop())

    bokeh_tornado = BokehTornado({'/bkapp': bkapp}, extra_websocket_origins=["localhost:8000", "%s:%d" % (NGINX_SERVER_ADDR,NGINX_SERVER_PORT)])
    bokeh_http = HTTPServer(bokeh_tornado)
    bokeh_http.add_sockets(sockets)

    server = BaseServer(IOLoop.current(), bokeh_tornado, bokeh_http)
    server.start()
    server.io_loop.start()

t = Thread(target=bk_worker)
t.daemon = True
t.start()
2 Likes

@_jm thank you for your detailed response with sample code. Appreciate it. I implemented everything in your note except for worker_processes, events and http as i got some syntax error thrown when i pasted it as-is in a conf file at /etc/nginx/conf.d/. However, i didn’t get the actual plot rendered. Maybe i did something else wrong.

Here is the screenshot:

I searched for a sample embed.html and used (see below), maybe it needs correction?

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Embedding a Bokeh Server With {{ framework }}</title>
</head>

<body>
  <div>
    This Bokeh app below served by a Bokeh server that has been embedded
    in another web app framework. For more information see the section
    <a  target="_blank" href="https://docs.bokeh.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library">Embedding Bokeh Server as a Library</a>
    in the User's Guide.
  </div>
  {{ script|safe }}
</body>
</html>

Someone suggested a modification as below, but didn’t make any difference

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Embedding a Bokeh Server With {{ framework }}</title>
  <link rel="stylesheet" href={​​​​​​{​​​​​​cdn_css | safe}​​​​​​}​​​​​​ type="text/css" />

         <script type="text/javascript" src={​​​​​​{​​​​​​cdn_js | safe}​​​​​​}​​​​​​></script>
</head>

<body>
  <div>
    This Bokeh app below served by a Bokeh server. For more information see
    <a  target="_blank" href="https://docs.bokeh.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library">Embedding Bokeh Server as a Library</a>
    in the User's Guide.
  </div>
  {{ script|safe }}
</body>
</html>

Here is the nginx conf i have:

server {
        listen       80;
        server_name  ##.##.##.##;

        location / {
            proxy_pass http://127.0.0.1:5100;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_http_version 1.1;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host:$server_port;
            proxy_buffering off;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

In the gunicorn_config.py i set nginx address to my public ip and port to 80 (and after running gunicorn and restarting nginx, went to my browser and typed my.pub.lic.ip which showed the error page) I didnt make any changes to your flask_gunicorn_embed.py

Any ideas on what could be going wrong? Would be great if you can share a suitable embed.html.

@Gopi_M

For that example, I used the embed template and theme (theme.yaml) from the Github pages for Bokeh’s official server embedding examples at the following URL.

https://github.com/bokeh/bokeh/tree/branch-2.3/examples/howto/server_embed

I would first check if there are any errors in the terminal window where the server is executed.

thanks @_jm for the quick response. I see that the embed html i used is the same one you referred to. I am not seeing any error msgs on the terminal window (in fact, no msgs at all) when i click my.pub.lic.ip on the browser.

@_jm, @Bryan

I am now able to config nginx + flask + bokeh for http only with the nginx config i mentioned above with flask code as below:

from flask import Flask, render_template
from bokeh.embed import server_document,server_session
from bokeh.client import pull_session
from flask_cors import CORS
import logging

app = Flask(__name__)
CORS(app)
logging.getLogger('flask_cors').level = logging.DEBUG

#create index page function
@app.route("/")
def index():
    myurl = "http://my.pub.lic.ip:5006/env"
   # myurl = "https://my.domain.net/env"
    bokeh_script = server_document(myurl)
    return render_template("index.html",bokeh_script=bokeh_script)

#run the app
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5003)

But the issue now to make this work with https as the main site is in https. I am able to config nginx to render bokeh app on https (i.e. nginx + bokeh only). I am able to host other Flask apps (without bokeh) thru nginx as well on https (i.e. nginx + flask) but not able to config nginx + flask + bokeh.

The main issue at hand for me is to deal with http to https exchange in the above flask code. When i configure nginx for ssl, the above flask code creates problem bcos it is sending http to the browser to get the bokeh_script. I tried CORS option as shown in code above but it didn’t work. I tried rendering bokeh app with --use-xheaders and even --allow-websocket-origin=*, but that also didn’t matter.

In the above Flask code, when i change myurl to https://mydomain/env i get error msg due to CORS policy and need a way to overcome that. If that happens then my problem might get resolved.

Access to XMLHttpRequest at 'https://mydonmain.net/env/autoload.js?bokeh-autoload-element=1004&bokeh-app-path=/env&bokeh-absolute-url=https://mydomain.net/env' from origin 'http://##.##.###.#:5003' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Any advice here would be of great help.

@Gopi_M

There are a lot of variables at play when trying to use https with nginx + flask + bokeh, including the environment where deployed, how nginx is configured (e.g. terminating SSL connections), and how bokeh is embedded.

Without any insight into the details of your setup, a few thoughts of things to try based on an inspection of the excerpts in this thread.

In the most recent post, it looks like the complaint is related to mixed http/https protocols, and the Flask app at :5003 is being run under http. What happens if you add an SSL context argument to the app( ) instantiation call? Something like the following

#run the app
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5003, ssl_context=('cert.pem','key.pem'))

where the cert.pem and key.pem are the certificate and key files.

Also, if you’re using self-signed certificates, note the following.

I originally had problems running a bokeh server securely using Gunicorn+Flask and an HTTPS address. In my case, the issue came down to needing to generate a Subject Alternative Name (SAN) certificate when I made a self-signed certificate via openssl for testing.

If you’re using a self-signed certificate, that might be what you’re running into as well. See this topic on bokeh discourse towards the end of the discussion for a link to more info.

https://discourse.bokeh.org/t/help-needed-with-ssl-for-a-nginx-gunicorn-flask-bokeh-setup/6069/8

Lastly, if those suggestions do not resolve the issue you might try setting the log level to debug for JavaScript Bokeh JS code, via BOKEH_LOG_LEVEL environment variable described here …

https://docs.bokeh.org/en/latest/docs/reference/settings.html

to see if that gives any further insight into things

thanks @_jm.
I tried with ssl_context='adhoc' and it now gives this error in bokeh terminal

404 GET /autoload.js?bokeh-autoload-element=1000&bokeh-app-path=/env&bokeh-absolute-url=https://mydomain.net/env

(PS: I actually have proper cert.pem and pvt.key but not sure of how to mention their paths, so resorted to ‘adhoc’ for now).

I am still not sure why it didn’t show the bokeh app in the nginx + flask config you suggested. It only showed the content in html without the bokeh_script.

I am summarizing my earlier problems here (excluding above 404):

  1. if i set myurl in my flask code as http:11.11.11.11:5006/env then i get error from browser saying content shouldn’t be served from http
  2. if i try to keep it as https:my.domain/env then i get CORS error.

https://my.domain.net/env/autoload.js?bokeh-autoload-element=1000&bokeh-app-path=/env&bokeh-absolute-url=https://my.domain.net/env’ from origin ‘http://##.##.###.#:5003’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

If there is a solution for fixing this CORS issue then i think we will get somewhere.

The core of the issue seem to be that flask sends the url to the browser to get the bokeh app (unlike nginx proxy pass which works from https to backend http)

@Bryan for your ref as well.

@Gopi_M

I am afraid there’s no way for me to provide any constructive help beyond the preceding recommendations with the partial visibility into the environment and the differing failure modes they cause.

It seems that things are failing due to mixed protocols, connection refusal, or the most recent 404: Not Found error.

For what it’s worth, when embarking on my implementations initially, I did experience CORS errors and experimented with using the Flask CORS extension as you have highlighted. But, in any of my ultimate deployment scenarios (locally, deployed in Heroku, etc.) this never was ultimately necessary.

In your latest simplified example with the Flask development server, how are you actually running the bokeh server?

If it is separately via bokeh serve ..., are you explicitly setting the address and port where it runs consistent with your flask app? What messages do you see in the terminal window where bokeh server is running? Have you enabled the xheaders option? Have you looked at log files with the log level set to debug as mentioned above?

hi @_jm, Yes, i am running it as bokeh serve with --allow-websocket-origin=* (just to ensure nothing is blocking) also tried with --use-xheaders. When i am getting these cors erros there is no reaction in bokeh terminal. In some other cases I see the same 404 msg in the terminal which i mentioned above.
I would like to think that the address and port are not an issue, as i am able to host it with same flask + bokeh apps combo with nginx without any issue but ‘on http’. The moment i configure nginx to listen on 443 it complains when flask app tries to send http url to browser for bokeh app. And if i try to make flask https by adding ssl_context, the CORS issue comes back :slight_smile:

@Bryan, @_jm I think the root cause is with the issue of CORS and nothing to do with nginx.

https://my.domain.net/env/autoload.js?bokeh-autoload-element=1000&bokeh-app-path=/env&bokeh-absolute-url=https://my.domain.net/env’ from origin ‘http://##.##.###.#:5003’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

If this gets resolved then my problem goes away :slight_smile:

When i configure flask app with ssl, and use https:domain.net/env url for fetching bokeh_script the above error msg is thrown which has probably got nothing to do with nginx. Is there any other configuration to be done on bokeh app (or flask) to overcome this issue? I am not sure how this is happening even after using --allow-websocket-origin=* while serving bokeh.

Just like we could hack Flask to be served from https://localhost:5000 (using ssl_context), is there a way to have bokeh accessible from https:localhost:5006? that could resolve the CORS issue as flask and bokeh would both be under same machine and on https.

I have nothing else to add unless there are some suggestions.

@Gopi_M You can definitely configure a Bokeh server for direct SSL termination, that is described here:

Running a Bokeh server — Bokeh 2.4.2 Documentation

However, things still may not work (I’m not sure) without the CORS headers set, and AFAIK there is no way to configure Bokeh server to send extra response headers (there probably should be, a GH issue might be appropriate). In case that is the case, the other idea is to not have the Bokeh server serve up the BokehJS resources at all. Some possibilities:

  • Let nginx do it

    location /static {
       alias /path/to/bokeh/server/static;
    }     
    

    You’ll need to make sure nginx has perms to access the BokehJS resources inside the Python package (or copy the static dir out to some other location for nginx to use). I think you could then configure nginx to respond with any necessary CORS headers as well.

  • Use CDN resources instead. Simples to set then environment variable:

    BOKEH_RESOURCES=cdn
    

    Then the pages will load BokehJS from cdn.bokeh.org which does set CORS headers.

@Gopi_M

For the final setup this topic landed on, namely nginx+flask+bokeh with HTTPS, I would circle back to the possibility that you can do this with a cloud or platform-as-a-service such as Heroku.

For setups like you have with the server running separately, Heroku was the only one that I found worked.

If you’re able to refactor things and invoke the server programmatically as a worker thread in the flask app, then others cloud services work and the CORS issues also disappear when running serving locally in that case, if I recall correctly.

If you did try Heroku, the way you would need to set it up is to have two separate dynos, one running the flask app and the other running the bokeh server via bokeh serve ...

This will give you HTTPS without having to manage yourself. And, given the above-mentioned architecture, you can run the bokeh server dyno with the --allow-websocket-origin set to the URL of the Flask app so that it is the only way to connect to the server.

Heroku also has an NGINX buildpack as one of its extensions too.

Admittedly, this would require a small expenditure of time to get set up with Heroku deployment, but it is well documented in my opinion.

As of a few months ago, they had a free-level (no cost) for dynos and a next-tier hobby-level, which cost approx. $7/US per dyno per month. I think the main thing with the hobby level is that your app doesn’t have to wake up when someone hits the site. And you can scale up from there through as your needs require.

hi @_jm, in my case nginx, flask and bokeh are all on the same server which has public ip. While the heroku cost is not a matter, bringing another provider is not an option. This VM setup is on aws and only accessible through our company vpn.

@Bryan, as i mentioned earlier i am able to have nginx + bokeh configured successfully for SSL and that is how my https://my.domain.net/env points to my bokeh app but we get in to all these https to http issues when flask app running on same vm at 5003 sends this url to browser (and we get CORS error mentioning that my.public.ip:5003 is not allowed to access bokeh resources at https://my.domain.net/env). I wanted to try direct ssl termination for bokeh as you suggested, but i only have .pem and .key files (from the certbot) created for mydomain.net and when i tried to use those certs it gave (probably rightfully) below error:
Cannot start Bokeh server [EACCES]: PermissionError(13, 'Permission denied')

To clarify, the point of the following statement was that the bokeh server is running as a separate process, i.e. invoked by bokeh serve ...; not that it is running on a separate computer system.

and the point of the following paragraph was that if your application is such that you can refactor it along the lines of the bokeh server examples on github, namely by using a worker thread and running the server programmatically via Serve(), then you might be able to do things locally too and avoid the CORS header limitations. I believe I started there before deciding to move to a provider and letting them handle the HTTPS and certificates.

@_jm Sure. I am currently running by bokeh app through bokeh serve --allow-origin-websocket command