Google's Cloud Run platform started supporting websockets recently which now makes it possible to run interactive Bokeh apps. Cloud Run is a fully managed platform to run containerized applications. I've been using it for my hobby projects as I do not have to worry about setting up virtual machine instances or any other infrastructure. The number of running containers are scaled up or down automatically as needed so you only pay for the time your app is being used. If you don't have a lot of traffic to your app, in most cases you will end up paying close to nothing. The chart below is taken from my Cloud Run dashboard to show how the number of containers scales over time.
To get started, we first need to install the Google Cloud SDK (gcloud
) by following the steps outlined here: https://cloud.google.com/sdk/docs/install
Next, initialize gcloud by setting up GCP login credentials.
gcloud init
As you follow the prompts, you can chose to create a new configuration or re-initialize the existing configuration. When asked, log in with your GCP account and select a project. The prompt also allows you to create a new project at this step if you don't already have a project you'd like to use.
If you run into any issues, here's the original article I referenced to go through these steps: https://cloud.google.com/sdk/docs/initializing
Before starting to write our application, we need to set up a few more things to ensure we have permissions to use the necessary services.
First, navigate to https://console.cloud.google.com/billing/projects and add a billing account to your project.
Next, enable required APIs by clicking on the Enable the APIs
link in this guide. That link will allow you to only set up the APIs required for deploying on Cloud Run without having to worry about all other APIs available on GCP so I highly recommend following the Enable the APIs
link from that guide. Select your GCP project from the drop-down menu and click Continue.
And finally, we need to set up required IAM permissions by clicking on the Go to Cloud Build Settings Page
link in this guide https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run
On the pop-up menu, click GRANT ACCESS TO ALL SERVICE ACCOUNTS
.
The steps we have taken so far only need to be done once when you start a new project. Now that we have all of the project settings and permissions set up, we can start building our Bokeh app. We'll build it as a directory application so that it's easy to add complexity later as we build on top of the initial application.
To create a minimal application, all we need is a folder with a main.py
file that contains the Bokeh app.
We will be deploying a simple app that I have directly taken from the Bokeh documentation, so we don't need to spend too much time on it. My main.py looked like below.
# Source: https://github.com/bokeh/bokeh/blob/branch-2.4/examples/app/sliders.py
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput
from bokeh.plotting import figure
# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# Set up plot
plot = figure(height=400, width=400, title="my sine wave",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
# Set up widgets
text = TextInput(title="title", value='my sine wave')
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)
# Set up callbacks
def update_title(attrname, old, new):
plot.title.text = text.value
text.on_change('value', update_title)
def update_data(attrname, old, new):
# Get the current slider values
a = amplitude.value
b = offset.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w) + b
source.data = dict(x=x, y=y)
for w in [offset, amplitude, phase, freq]:
w.on_change('value', update_data)
# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq)
curdoc().add_root(row(inputs, plot, width=800))
curdoc().title = "Sliders"
If we want to run this application locally, we can navigate to the parent directory and run the command bokeh serve demo
, but we need to add two more files to build and deploy this application to Cloud Run. One of them is Dockerfile that includes instructions to create a docker image that will be run by Cloud Run and the other is cloudbuild.yaml which includes instructions for the gcloud SDK on how to build the docker image, upload it to the Google Container Registry, and run it as a container service on Cloud Run. At the end of all this, our folder structure will look like this:
If you are not familiar with docker, I recommend watching this video on Youtube: https://www.youtube.com/watch?v=i7ABlHngi1Q&ab_channel=TravisMedia. It was quite useful to me as I was getting started last year.
Since we will be using Cloud Build to build and run our image, we don't need to have docker installed locally.
Let's start with the Dockerfile. We'll base our image on the official Anaconda image. Copy all our applications files and create a non-root user named bokeh that will run our application. It is always a good practice to not run container as root, especially when deploying to cloud services. The other key thing we need to do is to set the PORT environment variable and use it in the command to run the bokeh application. Cloud Run expects the application to run on post 8080 by default, whereas bokeh applications are run on post 5006 by default. By using the port variable, we are instructing Bokeh to run the application on port 8080.
FROM continuumio/anaconda3
WORKDIR /apps
COPY . .
ENV PORT=8080
ARG USER=bokeh
RUN useradd -s /bin/bash -m ${USER}
USER ${USER}
CMD bokeh serve --port $PORT demo
Once we have the Dockerfile
set up, we can write cloudbuild.yaml
. This file includes three steps:
You can pretty much copy all of the contents of this file below, but you will probably want to change the container image name bokeh-app-image
and the Cloud Run service name bokeh-app-service
to whatever makes sense in your case.
steps:
# Build the container image
- name: "gcr.io/cloud-builders/docker"
args: ["build", "-t", "gcr.io/$PROJECT_ID/bokeh-app-image", "."]
# Push the container image to Container Registry
- name: "gcr.io/cloud-builders/docker"
args: ["push", "gcr.io/$PROJECT_ID/bokeh-app-image"]
# Deploy container image to Cloud Run
- name: "gcr.io/cloud-builders/gcloud"
args: [
"run",
"deploy",
"bokeh-demo-app-service-1", # change to your service name
"--image",
"gcr.io/$PROJECT_ID/bokeh-app-image",
"--max-instances",
"1",
"--memory",
"128M",
"--concurrency",
"80",
"--region",
"us-central1",
"--platform",
"managed",
"--allow-unauthenticated",
]
images:
- gcr.io/$PROJECT_ID/bokeh-app-image
Now that we have both Dockerfile and cloudbuild.yaml, deploying our app is as easy as running the following shell command
gcloud builds submit
You will see a log output which should output the URL of the cloud run service from the deployment.
You can also navigate to https://console.cloud.google.com/run, click on the service and get the URL from the GCP console.
Alternatively, type the following command in a terminal to get the service URL
gcloud run services list --platform managed | awk 'NR==2 {print $4}'
You can now go to the URL to visit your Bokeh app. Unfortunately, the app won't load at this stage. Bokeh blocks any incoming connections that aren't explicitly whitelisted. We have to edit the Dockerfile to make this change and deploy the service again. This is a workaround since you can't know the service URL until it is deployed. If you were planning on connecting a custom domain to the service, you could whitelist that domain before deploying the service and avoid having to go through the following steps.
The updated Dockerfile now has an additional --allow-websocket-origin
parameter in the CMD instruction. Make sure to exclude the protocol information https://
from the url when passing it to --allow-websocket-origin
FROM continuumio/anaconda3
WORKDIR /apps
COPY . .
ENV PORT=8080
ARG USER=bokeh
RUN useradd -s /bin/bash -m ${USER}
USER ${USER}
CMD bokeh serve --port $PORT demo --allow-websocket-origin bokeh-demo-app-service-1-f56pgsgjvq-uc.a.run.app
Once again, deploy the service.
gcloud builds submit
Go to the URL and visit your bokeh app. Here's the URL of the bokeh service I created for this demo: https://bokeh-demo-app-service-1-f56pgsgjvq-uc.a.run.app
Next, let's try connecting a custom domain to our Bokeh app. The first step is to verify ownership of the domain. If you purchased your domain from Google Domains using the same Google account used for the GCP project, your domain is likely to be already verified. To check your list of verified domains, type the following command in the terminal.
gcloud domains list-user-verified
If you don't see your domain in the output, you will need to verify the domain first. You can do that with the following command.
gcloud domains verify example.com
The above command will open a brower tab and give you instructions on how to verifiy the domain. You will need to log into your account at the domain name provider and add a TXT record to the DNS configuration. If you run into any issues at this step, please refer to this article: https://support.google.com/a/answer/183895
Once the domain is verified with Google, we can map our Cloud Run service to the domain. The service name was first used in cloudbuild.yaml
in the following line:
"bokeh-demo-app-service-1", # change to your service name
Type the following command to create a service to domain mapping. Make sure to only include the root domain name without any protocol information (e.g. https
) or subdomain (e.g. blog.example.com
).
You will be prompted to select service region. We'll use the same same region we previously deployed our service in: us-central1
.
gcloud beta run domain-mappings create --service bokeh-demo-app-service-1 --domain example.com
If successful, you will see instructions on the DNS records that need to be updated. I needed to add four A
and four AAAA
records to my DNS settings.
I've added a screenshot of my Google Domains DNS settings below for reference.
Once the domain is mapped, we also need to make sure that our Bokeh app will allow connections from this new domain. In Dockerfile
, add the custom domain to the list of --allow-websocket-origin
arguments. The update Dockerfile
should look like this:
FROM continuumio/anaconda3
WORKDIR /apps
COPY . .
ENV PORT=8080
ARG USER=bokeh
RUN useradd -s /bin/bash -m ${USER}
USER ${USER}
CMD bokeh serve --port $PORT demo \
--allow-websocket-origin bokeh-demo-app-service-1-f56pgsgjvq-uc.a.run.app \
--allow-websocket-origin example.com
Let's deploy our Cloud Run service one more time.
gcloud builds submit
We can now navigate to our custom domain to access our Bokeh app.
I've embedded the app in an iframe below, so you can see it in action here!
from IPython.display import IFrame
IFrame(src='https://bokeh-demo-app-service-1-f56pgsgjvq-uc.a.run.app', width=1000, height=700)