I have a bokeh server dashboard with 15 or so endpoints that all reuse the same plotting function with different data. It is currently a flask webpage with the bokeh server embedded into it (though not married to that, could drop flask and just do bokeh maybe if flask is the root of this, but I don’t think it is). I want to put it into a docker container now, but when I do, and navigate in my browser to my endpoints, the page loads, but it doesn’t load the bokeh figures. The developer tools gives me “net::ERR_EMPTY_RESPONSE”. I am using the flask_gunicorn_embed.py example from the bokeh docs as my a base, and that dynamically maps the ports to whatever is an open port, since it doesn’t know how many ports you will need, depending on how many endpoints and such. Which I think is my problem. On the outside of the container the browser is looking for 127.0.0.1/12345/app_name, which is empty, since that is on the inside of the container.
Is there an easy fix to this? Should I just map each port manually? That won’t impact multiple users accessing the same endpoints right?
Here are some code examples. I would have made a gist, but I got a new phone and lost my 1 factor auth, and it says it will take 4-5 days for them to get me reauthorized.
app/main.py
try:
import asyncio
except ImportError:
raise RuntimeError("This example requries Python3 / asyncio")
from threading import Thread
from flask import Flask, render_template, Blueprint
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.plotting import figure
from bokeh.server.server import BaseServer
from bokeh.server.tornado import BokehTornado
from bokeh.server.util import bind_sockets
app = Flask(__name__)
def bkapp(doc):
plot = figure()
plot.line([1, 2, 3], [3, 2, 1])
doc.add_root(column(plot))
@app.route('/', methods=['GET'])
def bkapp_page():
name = bkapp.__name__
port = start_bk_worker(bkapp, name)
script = server_document(f'http://localhost:{port}/{name}')
return render_template("embed.html", script=script)
def start_bk_worker(bokeh_app, app_name):
sockets, port, app, = _process_bokeh_app(bokeh_app)
print(f"sockets {sockets} port {port} app {app}")
t = Thread(target=_bk_worker, kwargs=dict(sockets=sockets, app=app, app_name=app_name))
t.daemon = True
t.start()
return port
def _process_bokeh_app(bokeh_app):
app = Application(FunctionHandler(bokeh_app))
sockets, port = bind_sockets("localhost", 0)
return sockets, port, app
def _bk_worker(sockets, app, app_name):
asyncio.set_event_loop(asyncio.new_event_loop())
bokeh_tornado = BokehTornado(
{f"/{app_name}": app}, extra_websocket_origins=["127.0.0.1:5000", "localhost:5000"])
bokeh_http = HTTPServer(bokeh_tornado)
bokeh_http.add_sockets(sockets)
server = BaseServer(IOLoop.current(), bokeh_tornado, bokeh_http)
server.start()
server.io_loop.start()
app/templates/embed.html
<!doctype html>
<html lang="en">
<body>
{{ script|safe }}
</body>
</html>
requirements.txt
bokeh
Flask
main.py
from app.main import app
if __name__ == '__main__':
app.run()
Dockerfile
FROM python:3.8-slim-buster
RUN adduser bokeh
# Install dependencies
RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y gcc libpq-dev python3-dev
WORKDIR /home/bokeh
# Setup Python env
COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN pip install -r requirements.txt
RUN pip install gunicorn
# Copy the application over
COPY app app
COPY *.py entrypoint.sh ./
RUN chmod +x entrypoint.sh
# Setup the framework and run it
ENV FLASK_APP main.py
RUN chown -R bokeh ./
USER bokeh
EXPOSE 5000
ENTRYPOINT ["./entrypoint.sh"]
entrypoint.sh
#!/bin/bash
exec gunicorn --bind 0.0.0.0:5000 --access-logfile - --error-logfile - --log-level "INFO" --forwarded-allow-ips="*" main:app
then run on command line
docker build -t test:latest .
docker run -p 5000:5000 test:latest