Serving bokeh apps embedded in django from subpath

Hello, first time posting here. Hoping for some help with an issue with deploying bokeh apps embedded in a Django app.

I followed along with the how-to guide here, and had no issues when I put the django app at the root path (e.g. example.com). I’m now trying to put it in a subfolder (e.g. example.com/app/ghg) and getting an error when (I think) the charts are being called. Everything else works, including all non-bokeh static assets. Here are the messages in the dev tools console where it breaks down:

Bokeh: BokehJS not loaded, scheduling load and callback at 
Date Mon Sep 06 2021 23:20:34 GMT-0400 (Eastern Daylight Time)
93223d28-c289-4941-9ca8-8d370f964104:48:13
Bokeh: injecting script tag for BokehJS library:  /static/js/bokeh.min.js?v=c77564ca8be6e4b5a67648d980fe180d62bd99ccfc01bd272a3e708b9533f34b1c86b24a1a28c86555f838fa26e429697aad84c411cac25f0472231ed56f1eab 93223d28-c289-4941-9ca8-8d370f964104:88:15
Bokeh: injecting script tag for BokehJS library:  /static/js/bokeh-widgets.min.js?v=d69578f4c708833aad7a87f790b0d97fa6d93fa349ec23ce61e939df04628884effaa01033d76cf60ea1fcddec118e840a2544fa1ee50c6649033e6a2a65c3c9 93223d28-c289-4941-9ca8-8d370f964104:88:15
Bokeh: injecting script tag for BokehJS library:  /static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed 93223d28-c289-4941-9ca8-8d370f964104:88:15
GEThttp://localhost:8010/favicon.ico
[HTTP/1.1 404 Not Found 0ms]

failed to load /static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed 93223d28-c289-4941-9ca8-8d370f964104:60:15
Loading failed for the <script> with source “http://localhost:8010/static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed”. population-and-development-patterns:1:1
failed to load /static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed 93223d28-c289-4941-9ca8-8d370f964104:60:15
Loading failed for the <script> with source “http://localhost:8010/static/js/bokeh.min.js?v=c77564ca8be6e4b5a67648d980fe180d62bd99ccfc01bd272a3e708b9533f34b1c86b24a1a28c86555f838fa26e429697aad84c411cac25f0472231ed56f1eab”. population-and-development-patterns:1:1
failed to load /static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed 93223d28-c289-4941-9ca8-8d370f964104:60:15
Loading failed for the <script> with source “http://localhost:8010/static/js/bokeh-widgets.min.js?v=d69578f4c708833aad7a87f790b0d97fa6d93fa349ec23ce61e939df04628884effaa01033d76cf60ea1fcddec118e840a2544fa1ee50c6649033e6a2a65c3c9”. population-and-development-patterns:1:1

It seems to me that the issue is that the js script is trying to still use /static/ as the url, rather than the subfolder I want, /app/ghg/static, but I’m not sure where I can get it to modify this. I tracked the “failed to load” error to this: https://github.com/bokeh/bokeh/blob/7047c6a90535564c9d05121fc8a095aef1de3c21/bokeh/core/_templates/autoload_js.js#L78-L80

Any thoughts on how to solve this? I’m adding the changes I made between the root vs subfolder setup below - I think this is all correct as far as these settings go, I’m guessing that I need to modify some other setting but I’m not sure where that would be.

nginx config, with root path:

server {
    location / {
        proxy_pass http://unix:/run/ghg.sock;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }

    location /static {
      alias /srv/ghg/static;
    }
}

nginx config, with subfolder (/app/ghg added to locations):

server {
    location /app/ghg {
        proxy_pass http://unix:/run/ghg.sock;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }

    location /app/ghg/static {
      alias /srv/ghg/static;
    }
}

systemd service file that starts daphne server, root version:

[Unit]
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/srv/ghg
Environment="PATH=/srv/ghg/ve/bin"
ExecStart=/srv/ghg/ve/bin/daphne -u /run/ghg.sock --proxy-headers config.asgi:application

[Install]
WantedBy=multi-user.target

systemd daphne, with subfolder (–root-path added):

[Unit]
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/srv/ghg
Environment="PATH=/srv/ghg/ve/bin"
ExecStart=/srv/ghg/ve/bin/daphne -u /run/ghg.sock --proxy-headers --root-path=/app/ghg config.asgi:application

[Install]
WantedBy=multi-user.target

And the bokeh_apps list in urls.py, root version:

from django.apps import apps
from django.urls import path

from bokeh.server.django import autoload

from main import views

bokeh_app_config = apps.get_app_config("bokeh.server.django")

urlpatterns = [
    path("", views.index, name="index"),
    path("population-and-development-patterns/", views.pop_dev_patterns, name="pop")
]

bokeh_apps = [
    autoload("population-and-development-patterns", views.pop_dev_patterns_handler),
]

urls.py, with subfolders (app/ghg added to bokeh_apps - not sure how to get around adding this manually):

...
bokeh_apps = [
    autoload("app/ghg/population-and-development-patterns", views.pop_dev_patterns_handler),
]

A further note/some confirmation: in the subfolder scenario, I changed the location for the static url in the nginx config from location /app/ghg/static ... to location /static ... and the charts now load (though unsurprisingly now the rest of my static assets do not).

I’m in a bit of a rush. Quick thoughts:

  • If you are not in an airgapped situation, the very simplest solution is probably to just use CDN resources. This frees the sever and NGinx from having to serve them and also will have better browser caching behavior

  • Otherwise, if you are taking over /static then you would need to copy BokehJS files into that new location as a step in your production deployment.

Thanks for taking the time to look at this.

Maybe I don’t understand what you’re saying, but I’m not trying to use /static/ as the url (rather /app/ghg/static) but that is where the autoload.js script appears to be looking. So for instance in the console output above, if I take:

http://localhost:8010/static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed”

and copy it into the location bar and add in the correct path:

http://localhost:8010/app/ghg/static/js/bokeh-tables.min.js?v=7df86bd59a12d1287abccaaf550dc63dbcc732ef6896569394287b5548583a7ce78cb09753c8028248df647525515709d706e273f6c7c424ef52bbc7df4f71ed”

it will be found and the text of that script will be displayed. The problem is that bokehjs(?) is not looking for it there.

I can kind of hack it and point nginx to the same static filesystem directory twice, so that it loads both bokeh static content and django static content (the two locations I mentioned above, one for /static and one for /app/ghg/static, both aliased to /srv/ghg/static), but that’s definitely a less-than-ideal solution.

I’m afraid I don’t actually know enough about the Django integration to really comment more. In a typical bokeh serve app the page loads a relative static/js URL. Perhaps @mateusz can comment. Otherwise I’d really just re-iterate that switching to CDN resources would definitely be my advised solution since then the Bokeh server or NGinx do not have to be in the loop at all.