Reverse-proxying: Flask app with Bokeh server on Nginx Reverse-proxying: Flask app with Bokeh server on Nginx flask flask

Reverse-proxying: Flask app with Bokeh server on Nginx


I found the solution with help from Francis Daly of the forum.nginx.org and Bryan Van de ven from the Bokeh google group.

Essentially, the Bokeh app was not configured correctly behind the Nginx server. Here's what I did to make it work:

  • renamed the Bokeh app from company_abc.py to company_abc-app.py to differentiate between the app and the username, which is used in the URI.
  • To run the Bokeh app I used the option --use-xheaders
  • changed some --allow-websocket-origins and --host options in the bokeh serve call, and let it run on localhost:5006
  • in the Nginx config file, I declared a specific location for the Bokeh app (/company_abc-app/) and put it before the location /
  • In the Flask app I pull the Bokeh session from localhost:5006, but then change the URL for the autoload_server to include https, i.e. https://example.com, which was the crux, I think.

Here are the files that work:

'/etc/nginx/sites-available/default':

upstream flask_siti {        server 127.0.0.1:8118 fail_timeout=0;}upstream bokeh_siti {        server 127.0.0.1:5006 fail_timeout=0;}server {            listen 443 ssl;            server_name example.com www.example.com;            ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;            ssl_protocols TLSv1 TLSv1.1 TLSv1.2;        ssl_prefer_server_ciphers on;        ssl_dhparam /etc/ssl/certs/dhparam.pem;        ssl_ciphers '###';        ssl_session_timeout 1d;        ssl_session_cache shared:SSL:50m;        ssl_stapling on;        ssl_stapling_verify on;        add_header Strict-Transport-Security max-age=15768000;        charset utf-8;        client_max_body_size 75M;        access_log /var/log/nginx/flask/access.log;        error_log  /var/log/nginx/flask/error.log;        keepalive_timeout 5;        location /company_abc-app/ {                  proxy_pass http://bokeh_siti;                  proxy_set_header Upgrade $http_upgrade;                  proxy_set_header Connection "upgrade";                  proxy_http_version 1.1;                  proxy_set_header X-Forwarded-Proto $scheme;                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;                  proxy_set_header Host $host:$server_port;                  proxy_buffering off;        }        location / {                # checks for static file, if not found proxy to the app                try_files $uri @proxy_to_app;        }        location @proxy_to_app {                  proxy_pass http://flask_siti;                  proxy_set_header Upgrade $http_upgrade;                  proxy_set_header Connection "upgrade";                  proxy_http_version 1.1;                  proxy_set_header X-Forwarded-Proto $scheme;                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;                  proxy_set_header Host $host:$server_port;                  proxy_buffering off;        }}server {    listen 80;    server_name example.com www.example.com;    return 301 https://$host$request_uri;}

'/etc/supervisor/conf.d/bokeh_serve.conf':

[program:bokeh_serve]command=/opt/envs/virtual/bin/bokeh serve company_abc-app.py --allow-websocket-origin=example.com --allow-websocket-origin=www.example.com --port=5006 --host=example.com:443 --host=www.example.com:443 --host=127.0.0.1:5006 --use-xheadersdirectory=/opt/webapps/flask_telemetryautostart=falseautorestart=truestartretries=3user=nobody

'/etc/supervisor/conf.d/flask.conf'

[program:flask]command=/opt/envs/virtual/bin/gunicorn -b :8118 website_app:appdirectory=/opt/webapps/flask_telemetryuser=nobodyautostart=trueautorestart=trueredirect_stderr=true

And here is my Flask app (Note that I hashed out security related info):

from flask import Flaskfrom flask_sqlalchemy import SQLAlchemyfrom flask import render_template, request, redirect, url_forfrom flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, login_required, roles_accepted, current_userfrom flask_security.decorators import anonymous_user_requiredfrom flask_security.forms import LoginFormfrom bokeh.embed import autoload_serverfrom bokeh.client import pull_sessionfrom wtforms import StringFieldfrom wtforms.validators import InputRequiredfrom werkzeug.contrib.fixers import ProxyFixapp = Flask(__name__)app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:password@localhost/telemetry'app.config['SECRET_KEY'] = '###'app.config['SECURITY_REGISTERABLE'] = Trueapp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = Falseapp.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = 'username'app.config['SECURITY_POST_LOGIN_VIEW'] = '/re_direct'app.debug = Truedb = SQLAlchemy(app)# Define modelsroles_users = db.Table('roles_users',        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))class Role(db.Model, RoleMixin):    id = db.Column(db.Integer(), primary_key=True)    name = db.Column(db.String(80), unique=True)    description = db.Column(db.String(255))class User(db.Model, UserMixin):    id = db.Column(db.Integer, primary_key=True)    username = db.Column(db.String(255), unique=True)    password = db.Column(db.String(255))    active = db.Column(db.Boolean())    confirmed_at = db.Column(db.DateTime())    roles = db.relationship('Role', secondary=roles_users,                            backref=db.backref('users', lazy='dynamic'))class ExtendedLoginForm(LoginForm):    email = StringField('Username', [InputRequired()])# Setup Flask-Securityuser_datastore = SQLAlchemyUserDatastore(db, User, Role)security = Security(app, user_datastore, login_form=ExtendedLoginForm)# # Create a user to test with# @app.before_first_request# def create_user():#     print('Create User')#     db.create_all()#     user_datastore.create_role(name='company_abc')#     user_datastore.create_role(name='admin')#     user_datastore.create_user(username='company_abc', password='###', roles=['company_abc'])#     user_datastore.create_user(username='admin', password='###', roles=['admin'])#     db.session.commit()#     print('User created')# Views@app.route('/')@anonymous_user_requireddef index():    return render_template('index.html')@app.route('/re_direct/')@login_requireddef re_direct():    identifier = current_user.username    print(identifier)    return redirect(url_for(identifier))@app.route('/index/')@login_required@roles_accepted('admin')def admin():    return render_template('admin.html')@app.route("/company_abc/")@login_required@roles_accepted('company_abc', 'admin')def company_abc():    url='http://127.0.0.1:5006'    session=pull_session(url=url,app_path="/company_abc-app")    url_https='https://company_abc.net'    bokeh_script=autoload_server(None,app_path="/company_abc-app",session_id=session.id,url=url_https)    return render_template("company_abc.html", bokeh_script=bokeh_script)app.wsgi_app = ProxyFix(app.wsgi_app)if __name__ == '__main__':    app.run()