Bokeh server APIs#

ASGI applications#

BokehASGI is a framework-neutral ASGI 3 application that can be served directly or mounted inside another ASGI application. Bokeh does not select or install an ASGI server or framework:

from pathlib import Path

from bokeh.server.asgi import BokehASGI

application = BokehASGI({"/plot": Path("bkapp.py")})

Save this as main.py and serve it using any ASGI 3 server, for example python -m uvicorn main:application. To mount it in FastAPI or Starlette:

from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI

from bokeh.server.asgi import BokehASGI

bokeh_app = BokehASGI({"/": Path("bkapp.py")})

@asynccontextmanager
async def lifespan(site):
    # Mounted FastAPI/Starlette applications don't receive lifespan events.
    await bokeh_app.core.start()
    try:
        yield
    finally:
        await bokeh_app.core.stop()

site = FastAPI(lifespan=lifespan)
site.mount("/bokeh", bokeh_app)

Path applications use the same formats as bokeh serve. A path may identify a Python script or a directory-style application containing main.py or main.ipynb. Directory applications also support app_hooks.py, server_lifecycle.py, static, templates/index.html, and theme.yaml. Application code runs once per session and modifies curdoc(). Relative paths are resolved from the server process’s working directory. Existing explicit application forms remain supported:

from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler

explicit = Application(FunctionHandler(modify_document))
BokehASGI({"/explicit": explicit, "/callable": modify_document})

The mount’s ASGI root_path is included automatically in Bokeh resource and websocket URLs. Equivalent complete examples are available for:

The ASGI frontend handles Bokeh document, autoload, metadata, static asset, and websocket routes, as well as application startup and shutdown through ASGI lifespan events.

Reverse proxy deployment#

A reverse proxy must preserve the public Host and browser-supplied Origin headers. Forward Upgrade, Connection, and Sec-WebSocket-Protocol unchanged; Bokeh’s websocket handshake requires the bokeh subprotocol followed by the session token. If the page origin is not the public Bokeh host, add that origin to extra_websocket_origins. If the proxy strips a public path prefix, configure the ASGI server or parent mount to put that prefix in the ASGI root_path so Bokeh generates matching resource and websocket URLs.

Only trust forwarded client, host, and scheme headers from known proxies. Configure websocket ping interval and timeout, proxy idle timeout, and maximum websocket message size at the ASGI server: ASGI does not expose portable ping frames or message-size controls to Bokeh.

Bokeh session documents and callbacks are process-local. Multi-worker deployments therefore need session affinity, and signed sessions need the same strong secret_key on every worker. External producers must deliver updates to every worker. When Bokeh is mounted, use the parent-lifespan pattern above only if the framework does not propagate lifespan events to mounted apps.

The following nginx and Apache configurations preserve a public /services/bokeh path when proxying to an ASGI server listening on port 5100. Configure the same prefix on the Bokeh application:

application = BokehASGI({"/myapp": modify_document}, prefix="/services/bokeh")

Both configurations are exercised by Bokeh’s nightly deployment tests. If your proxy strips the prefix instead, leave prefix unset and configure root_path as described above.

Nginx#

server {
    listen 8080;
    server_name _;

    location /services/bokeh/ {
        proxy_pass http://127.0.0.1:5100;
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Origin $http_origin;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;
        proxy_buffering off;
    }
}

Apache#

Listen 8080

<VirtualHost *:8080>
    ServerName localhost

    ProxyRequests Off
    ProxyPreserveHost On

    ProxyPass "/services/bokeh/myapp/ws" "ws://127.0.0.1:5100/services/bokeh/myapp/ws"
    ProxyPassReverse "/services/bokeh/myapp/ws" "ws://127.0.0.1:5100/services/bokeh/myapp/ws"

    ProxyPass "/services/bokeh/" "http://127.0.0.1:5100/services/bokeh/"
    ProxyPassReverse "/services/bokeh/" "http://127.0.0.1:5100/services/bokeh/"
</VirtualHost>

Updating active sessions from the host#

An enclosing ASGI application’s lifespan can own a single background producer and publish its output to every currently active Bokeh session. Pass update_sessions() a callable that accepts a single Document:

def update_document(doc):
    source = doc.get_model_by_name("shared-source")
    source.data = latest_snapshot

await bokeh_app.update_sessions("/", update_document)

Bokeh invokes update_document once per local session with that document’s lock held. The callable may be synchronous or asynchronous, and updates to different sessions run concurrently. Sessions created after the call starts are not included, so application construction should also initialize a new document from the latest snapshot. Shared data read by synchronous application code must be immutable or protected for access from worker threads.

This operation is process-local. Deployments with multiple ASGI workers need an external broker or data service to deliver each snapshot to every worker. See examples/server/api/asgi/fastapi_shared_data.py for a complete lifespan-managed example.

ASGI servers send lifespan events to the top-level application. When Bokeh is mounted under a framework that does not propagate those events to mounts, such as FastAPI or Starlette, compose bokeh_app.core.start() and bokeh_app.core.stop() into the parent lifespan as shown above.

Authentication#

Authentication can be performed by the host framework, by a AuthPolicy, or by both. An auth policy protects Bokeh’s dynamic HTTP routes and websocket handshake without importing an ASGI framework:

import os

from bokeh.server.asgi import BokehASGI
from bokeh.server.auth import AuthPolicy

async def authenticate(request):
    authorization = request.headers.get("authorization")
    if authorization == f"Bearer {os.environ['SITE_TOKEN']}":
        return "alice"
    return None

policy = AuthPolicy(
    authenticate,
    login_url="/login",
    logout_url="/logout",
)

application = BokehASGI(
    {"/": "bkapp.py"},
    auth_policy=policy,
    sign_sessions=True,
    secret_key=os.environ["BOKEH_SECRET_KEY"].encode(),
)

The authenticator may be synchronous or asynchronous. It returns the current user, or None to reject a request. Unauthenticated HTTP requests redirect to login_url, when configured, and otherwise receive HTTP 401. Unauthenticated websockets are closed before Bokeh accepts them. Login and logout endpoints remain the responsibility of the host application.

Authentication middleware such as Starlette’s commonly stores its result in the ASGI scope["user"] value. Bokeh copies this to request.user, so a policy can enforce the host framework’s result. Configure the parent application’s lifespan for this bokeh_app as shown above, then mount it:

def authenticate(request):
    user = request.user
    if user is not None and getattr(user, "is_authenticated", False):
        return user
    return None

bokeh_app = BokehASGI(
    {"/": "bkapp.py"},
    auth_policy=AuthPolicy(authenticate, login_url="/login"),
)
site.mount("/bokeh", bokeh_app)

The authenticated user is subsequently available as curdoc().session_context.request.user. ASGI scope["state"] is similarly available to the authenticator as request.state.

Session tokens are bearer credentials, not a replacement for authenticating HTTP and websocket requests. Authenticated deployments should enable signed sessions and configure a strong shared secret. Token payloads are signed but not encrypted; use include_headers, exclude_headers, include_cookies, and exclude_cookies to avoid copying secrets into them.

The older AuthProvider and --auth-module interfaces use Tornado request handlers and remain available for the Tornado frontend. They are not required by AuthPolicy.

Session document construction runs in worker threads. Consequently, expensive synchronous application code does not block the event loop from accepting unrelated HTTP or websocket work, and independent sessions can initialize concurrently. Script applications serialize the temporary process-global state they require, including sys.path, sys.argv, and the working directory.

Embedding in Tornado#

It can be useful to embed the Bokeh Server in a larger Tornado application, or a Jupyter notebook, and use the already existing Tornado IOloop. Here is the basis for integration of Bokeh in such a scenario:

from bokeh.server.server import Server

server = Server(
    bokeh_applications,  # list of Bokeh applications
    io_loop=loop,        # Tornado IOLoop
    **server_kwargs      # port, num_procs, etc.
)

# start timers and services and immediately return
server.start()

If your server should honor the same environment variables and configuration files used by bokeh serve (for example, BOKEH_AUTH_MODULE, BOKEH_SSL_CERTFILE, BOKEH_SIGN_SESSIONS), use the from_settings() factory method instead:

from bokeh.server.server import Server

server = Server.from_settings(
    bokeh_applications,  # list of Bokeh applications
    io_loop=loop,        # Tornado IOLoop
    **server_kwargs
)

server.start()

You can also create and control an IOLoop directly. This can be useful when creating standalone “normal” Python scripts that serve Bokeh apps or embedding a Bokeh application in a framework like Flask or Django without having to run a separate Bokeh server process. You can find some examples of this technique in the examples directory:

Also note that every command line argument for bokeh serve has a corresponding keyword argument for Server. For instance, using the --allow-websocket-origin command line argument is equivalent to passing allow_websocket_origin as a parameter.

Connecting with bokeh.client#

You can directly interact with the Bokeh server via a client API, which you can use to make modifications to Bokeh documents in existing sessions on a Bokeh server.

../../../_images/bokeh_serve_client.svg

Typically, web browsers connect to the Bokeh server, but you can make a connection from Python by using the bokeh.client module.#

This can be useful, for example, to make user-specific customizations to a Bokeh app that is embedded by another web framework, such as Flask or Django. In the following example, a Flask endpoint embeds a “sliders” app already running on the server but changes the plot title before passing the output to the user.

from flask import Flask, render_template

from bokeh.client import pull_session
from bokeh.embed import server_session

app = Flask(__name__)

@app.route('/', methods=['GET'])
def bkapp_page():

    with pull_session(url="http://localhost:5006/sliders") as session:

        # update or customize that session
        session.document.roots[0].children[1].title.text = "Special sliders for a specific user!"

        # generate a script to load the customized session
        script = server_session(session_id=session.id, url='http://localhost:5006/sliders')

        # use the script in the rendered page
        return render_template("embed.html", script=script, template="Flask")

if __name__ == '__main__':
    app.run(port=8080)