OS Whiteboarding Apps

Published: Dec 10, 2024 by Isaac Johnson

Recently I found myself wanting to whip up a quick interactive whiteboard without having to resort to sharing a google drawing in a Teams meeting or using Miro with it’s nauseating zoom in and outs.

In this post we will explore three options: Lorien, Whitebophir and Spacedeck. Lorien is a simple local fat client for making quick drawings or whiteboarding locally. Whitebophir and Spacedeck are very similar. Both are containerized, easy to host in Docker or Kubernetes, and could solve my need for a shareable meeting whiteboard.

Lorien

Let’s begin by looking at the Github page for Lorien.

I’ll start by downloading it to WSL

builder@LuiGi:~$ mkdir lorien && cd lorien && wget https://github.com/mbrlabs/Lorien/releases/download/v0.6.0/Lorien_v0.6.0_Linux.tar.xz
--2024-11-27 19:07:00--  https://github.com/mbrlabs/Lorien/releases/download/v0.6.0/Lorien_v0.6.0_Linux.tar.xz
Resolving github.com (github.com)... 140.82.114.4, 2607:7700:0:2d:0:2:8c52:7204
Connecting to github.com (github.com)|140.82.114.4|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://objects.githubusercontent.com/github-production-release-asset-2e65be/364580533/9b3a6e4f-a688-4543-af2d-0c708cb0fd7f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=releaseassetproduction%2F20241128%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241128T010700Z&X-Amz-Expires=300&X-Amz-Signature=e33a7bbdf15860742cead2a3452391c7f9b029b593b9f9c0a6f210161c7f886b&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3B%20filename%3DLorien_v0.6.0_Linux.tar.xz&response-content-type=application%2Foctet-stream [following]
--2024-11-27 19:07:00--  https://objects.githubusercontent.com/github-production-release-asset-2e65be/364580533/9b3a6e4f-a688-4543-af2d-0c708cb0fd7f?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=releaseassetproduction%2F20241128%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241128T010700Z&X-Amz-Expires=300&X-Amz-Signature=e33a7bbdf15860742cead2a3452391c7f9b029b593b9f9c0a6f210161c7f886b&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3B%20filename%3DLorien_v0.6.0_Linux.tar.xz&response-content-type=application%2Foctet-stream
Resolving objects.githubusercontent.com (objects.githubusercontent.com)... 185.199.111.133, 185.199.108.133, 185.199.109.133, ...
Connecting to objects.githubusercontent.com (objects.githubusercontent.com)|185.199.111.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 30753392 (29M) [application/octet-stream]
Saving to: ‘Lorien_v0.6.0_Linux.tar.xz’

Lorien_v0.6.0_Linux.tar.xz    100%[=================================================>]  29.33M  7.45MB/s    in 4.8s

2024-11-27 19:07:05 (6.07 MB/s) - ‘Lorien_v0.6.0_Linux.tar.xz’ saved [30753392/30753392]


builder@LuiGi:~/lorien$ tar -xf Lorien_v0.6.0_Linux.tar.xz
builder@LuiGi:~/lorien$ ./Lorien_v0.6.0_Linux/Lorien.x86_64 Lorien_v0.6.0_Linux
Lorien_v0.6.0_Linux/        Lorien_v0.6.0_Linux.tar.xz

I tried to to launch it - and it did come up for a moment

/content/images/2024/12/whiteboard-01.png

But then quickly segfaulted

builder@LuiGi:~/lorien$ ./Lorien_v0.6.0_Linux/Lorien.x86_64
Godot Engine v3.5.3.stable.official.6c814135b - https://godotengine.org
MESA: error: ZINK: failed to choose pdev
glx: failed to create drisw screen
MESA: error: ZINK: failed to choose pdev
glx: failed to create drisw screen
MESA: error: ZINK: failed to choose pdev
glx: failed to create drisw screen
OpenGL ES 3.0 Renderer: D3D12 (Intel(R) Iris(R) Xe Graphics)
Async. shader compilation: OFF
Segmentation fault (core dumped)

That said, on the releases page there is a windows binary too

This seemed to launch without issue

/content/images/2024/12/whiteboard-02.png

As you can see it works okay - granted i’m not as fast with a trackpad as mouse:

My big issue (and a simple feature to see them add) is that there is no text. If I could just quick label things without handwriting that would be great.

Microsoft Office Whiteboard

As part of Office365, there is a whiteboard.office.com which is a bit slow but does work and is online (so the slowness could come from my slow hotspot where I’m writing presently)

/content/images/2024/12/mswhiteboard-02.png

Unlike the first option, it does have text

Whitebophir

Another nice option I found that can run in Docker is Whitebophir

They have simple docker steps

mkdir wbo-boards # Create a directory that will contain your whiteboards
chown -R 1000:1000 wbo-boards # Make this directory accessible to WBO
docker run -it --publish 5001:80 --volume "$(pwd)/wbo-boards:/opt/app/server-data" lovasoa/wbo:latest # run wbo

I can convert that into a Decent kubernetes manifest

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wbo-boards-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wbo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wbo
  template:
    metadata:
      labels:
        app: wbo
    spec:
      containers:
        - name: wbo
          image: lovasoa/wbo:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: /opt/app/server-data
              name: wbo-boards
      volumes:
        - name: wbo-boards
          persistentVolumeClaim:
            claimName: wbo-boards-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: wbo-service
spec:
  selector:
    app: wbo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Then applied it

$ kubectl apply -f ./wbo-boards.yaml
persistentvolumeclaim/wbo-boards-pvc created
deployment.apps/wbo-deployment created
service/wbo-service created

Then port-forward to test

$ kubectl port-forward svc/wbo-service 5550:80
Forwarding from 127.0.0.1:5550 -> 80
Forwarding from [::1]:5550 -> 80

Which works

/content/images/2024/12/wboboard-01.png

This was quite easy to whip up a board

/content/images/2024/12/mswhiteboard-03.png

Actually, this was really performant. I’m going to kick a URL up for it next.

I’ll make a Route53 JSON

$ cat r53-whiteboard.json
{
  "Comment": "CREATE whiteboard fb.s A record ",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "whiteboard.freshbrewed.science",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "75.73.224.240"
          }
        ]
      }
    }
  ]
}

And have AWS R53 create the A record

$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-whiteboard.json
{
    "ChangeInfo": {
        "Id": "/change/C09040521537OOOFI2EHH",
        "Status": "PENDING",
        "SubmittedAt": "2024-11-28T01:43:27.345000+00:00",
        "Comment": "CREATE whiteboard fb.s A record "
    }
}

I’ll then create an Ingress YAML

$ cat ingress.whiteboard.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: whiteboard
spec:
  rules:
  - host: whiteboard.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: wbo-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - whiteboard.freshbrewed.science
    secretName: whiteboard-tls

and apply it

$ kubectl apply -f ./ingress.whiteboard.yaml
ingress.networking.k8s.io/whiteboard created

I can then wait on the cert to be satisified

$ kubectl get cert whiteboard-tls
NAME             READY   SECRET           AGE
whiteboard-tls   False   whiteboard-tls   37s

$ kubectl get cert whiteboard-tls
NAME             READY   SECRET           AGE
whiteboard-tls   False   whiteboard-tls   83s

$ kubectl get cert whiteboard-tls
NAME             READY   SECRET           AGE
whiteboard-tls   True    whiteboard-tls   113s

/content/images/2024/12/wboboard-02.png

Let’s see it in use:

I can create a “private” board which just makes a long unique URL

/content/images/2024/12/wboboard-04.png

Which I can just copy and paste the URL into another browser (or share with others to use)

/content/images/2024/12/wboboard-05.png

Spacedeck

Another interesting option is spacedeck-open

It has a docker-compose YAML

version: "2.0"
services:
  spacedeck:
    build: .
    container_name: spacedeck
    ports:
      - "9666:9666"
    volumes:
      - ./storage:/app/storage
      - ./database:/app/database

Let’s clone

builder@LuiGi:~/Workspaces$ git clone https://github.com/spacedeck/spacedeck-open.git
Cloning into 'spacedeck-open'...
remote: Enumerating objects: 2074, done.
remote: Counting objects: 100% (501/501), done.
remote: Compressing objects: 100% (151/151), done.
remote: Total 2074 (delta 401), reused 370 (delta 350), pack-reused 1573 (from 1)
Receiving objects: 100% (2074/2074), 5.09 MiB | 3.86 MiB/s, done.
Resolving deltas: 100% (1212/1212), done.

I’ll just fire it up

builder@LuiGi:~/Workspaces$ cd spacedeck-open/
builder@LuiGi:~/Workspaces/spacedeck-open$ ls
CHANGELOG.md  LICENSE    database            helpers       middlewares        package.json  spacedeck.js  yarn.lock
Dockerfile    README.md  docker-compose.yml  integrations  models             public        styles
Gulpfile.js   config     docs                locales       package-lock.json  routes        views
builder@LuiGi:~/Workspaces/spacedeck-open$ docker compose up
[+] Building 38.8s (1/2)                                                                                 docker:default
 => [spacedeck internal] load build definition from Dockerfile                                                     0.1s
 => => transferring dockerfile: 630B                                                                               0.0s
 => [spacedeck internal] load metadata for docker.io/library/node:18-alpine 

Before we try as YAML, we’ll need to push that image somewhere as the project does not have a published image.

builder@builder-T100:~/spacedeck-open$ docker build -t spacedeck:0.1 .
[+] Building 32.0s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                     0.0s
 => => transferring dockerfile: 630B                                                                                     0.0s
 => [internal] load metadata for docker.io/library/node:18-alpine                                                        0.9s
 => [auth] library/node:pull token for registry-1.docker.io                                                              0.0s
 => [internal] load .dockerignore                                                                                        0.0s
 => => transferring context: 157B                                                                                        0.0s
 => [1/7] FROM docker.io/library/node:18-alpine@sha256:7e43a2d633d91e8655a6c0f45d2ed987aa4930f0792f6d9dd3bffc7496e44882  2.5s
 => => resolve docker.io/library/node:18-alpine@sha256:7e43a2d633d91e8655a6c0f45d2ed987aa4930f0792f6d9dd3bffc7496e44882  0.0s
 => => sha256:aa6f657bab0c137ad8a7930532dbd7c53338023e828890090a4070f47a2ed65d 40.09MB / 40.09MB                         0.9s
 => => sha256:f477ea663f1c2a017b6f95e743b5ad72f8ff8bf861b0759a86ba1985f706308f 1.39MB / 1.39MB                           0.3s
 => => sha256:7e43a2d633d91e8655a6c0f45d2ed987aa4930f0792f6d9dd3bffc7496e44882 7.67kB / 7.67kB                           0.0s
 => => sha256:7000d2e73f938c4f62fdda6d398d7dffd50e6c129409ae2b1a36ccebf9289ffe 1.72kB / 1.72kB                           0.0s
 => => sha256:870e987bd79332f29e9e21d7fa06ae028cfb507bb8bbca058f6eb60f7111cae9 6.20kB / 6.20kB                           0.0s
 => => sha256:da9db072f522755cbeb85be2b3f84059b70571b229512f1571d9217b77e1087f 3.62MB / 3.62MB                           0.2s
 => => extracting sha256:da9db072f522755cbeb85be2b3f84059b70571b229512f1571d9217b77e1087f                                0.2s
 => => sha256:43c47a581c29baa57713ee0da7af754f3994227b64949cb5e9e4dbbc2108c6cd 449B / 449B                               0.3s
 => => extracting sha256:aa6f657bab0c137ad8a7930532dbd7c53338023e828890090a4070f47a2ed65d                                1.0s
 => => extracting sha256:f477ea663f1c2a017b6f95e743b5ad72f8ff8bf861b0759a86ba1985f706308f                                0.1s
 => => extracting sha256:43c47a581c29baa57713ee0da7af754f3994227b64949cb5e9e4dbbc2108c6cd                                0.0s
 => [internal] load build context                                                                                        0.1s
 => => transferring context: 7.72MB                                                                                      0.1s
 => [2/7] WORKDIR /app                                                                                                   0.8s
 => [3/7] RUN apk add --no-cache     chromium     nss     freetype     freetype-dev     harfbuzz     ca-certificates    11.8s
 => [4/7] RUN apk add graphicsmagick ffmpeg ffmpeg-dev ghostscript                                                       3.3s
 => [5/7] COPY package*.json ./                                                                                          0.1s
 => [6/7] RUN npm install                                                                                               10.7s
 => [7/7] COPY . .                                                                                                       0.1s
 => exporting to image                                                                                                   1.6s
 => => exporting layers                                                                                                  1.6s
 => => writing image sha256:aa9624bccb8b24264c419873c89af776556796acd513cc86287c2fea7c093928                             0.0s
 => => naming to docker.io/library/spacedeck:0.1                                  

I’ll then push that to my private CR

builder@builder-T100:~/spacedeck-open$ docker tag spacedeck:0.1 harbor.freshbrewed.science/freshbrewedprivate/spacedeck:0.1
builder@builder-T100:~/spacedeck-open$ docker push harbor.freshbrewed.science/freshbrewedprivate/spacedeck:0.1
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/spacedeck]
329089f994d9: Pushed
0663b90c631c: Pushed
605a5910c173: Pushed
732a9f505e99: Pushed
26b7725b6662: Pushed
c1226e3f2e76: Pushed
aae2f82b308d: Pushed
5003e477a3c5: Pushed
83ada9b02fd6: Pushed
75654b8eeebd: Pushed
0.1: digest: sha256:c33d126003c46e9f4915037fe66d0c61f56fdc477c5cead969efaea8f1f8d29f size: 2422

Let’s try as YAML

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: storage-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spacedeck-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spacedeck
  template:
    metadata:
      labels:
        app: spacedeck
    spec:
      imagePullSecrets:
       - name: myharborreg
      containers:
        - name: spacedeck
          image: harbor.freshbrewed.science/freshbrewedprivate/spacedeck:0.1
          ports:
            - containerPort: 9666
          volumeMounts:
            - mountPath: /app/storage
              name: storage
            - mountPath: /app/database
              name: database
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: storage-pvc
        - name: database
          persistentVolumeClaim:
            claimName: database-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: spacedeck-service
spec:
  selector:
    app: spacedeck
  ports:
    - protocol: TCP
      port: 9666
      targetPort: 9666

Let’s fire that up

$ kubectl apply -f kubernetes.yaml
persistentvolumeclaim/storage-pvc created
persistentvolumeclaim/database-pvc created
deployment.apps/spacedeck-deployment created
service/spacedeck-service created

I’ll move on to exposing the ingress so I can test.

First, I’ll create an A Record in Cloud DNS

$ gcloud dns --project=myanthosproject2 record-sets create spacedeck.steeped.sp
ace --zone="steepedspace" --type="A" --ttl="300" --rrdatas="75.73.224.240"
NAME                      TYPE  TTL  DATA
spacedeck.steeped.space.  A     300  75.73.224.240

Then fire up an ingress object

$ cat ./spacedeck.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: spacedeck-service
  name: spacedeckgcpingress
spec:
  rules:
  - host: spacedeck.steeped.space
    http:
      paths:
      - backend:
          service:
            name: spacedeck-service
            port:
              number: 9666
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - spacedeck.steeped.space
    secretName: spacedeckgcp-tls

Once the cert is validated

$ kubectl get cert spacedeckgcp-tls
NAME               READY   SECRET             AGE
spacedeckgcp-tls   True    spacedeckgcp-tls   2m52s

I can now test

/content/images/2024/12/spacedeck-01.png

But I cannot create an account without typing an “Invite code”. I tried a random “asdf”

/content/images/2024/12/spacedeck-02.png

It is actually set in a default JSON config

builder@LuiGi:~/Workspaces/spacedeck-open$ cat config/default.json | jq '.invite_code'
"top-sekrit"

I then used it for the signup

/content/images/2024/12/spacedeck-03.png

which worked

/content/images/2024/12/spacedeck-04.png

I can create a space to get started

/content/images/2024/12/spacedeck-05.png

This is surprisingly complete

/content/images/2024/12/spacedeck-06.png

Here we can see some collaboration between me using the website on my phone and then seeing the same thing locally in the laptop browser

or create and edit a new board

If I want to share it, I can generate an invite (assumes I set up email service) or get a URL

/content/images/2024/12/spacedeck-09.png

I set the board to be public and then put the URL in an incognito window

/content/images/2024/12/spacedeck-10.png

This could be handy for presentations or hnd generated architecture diagrams one might want to put into a webpage or wiki docs.

Just to make it easier for others to use, I tagged and pushed the container to dockerhub

builder@builder-T100:~/spacedeck-open$ docker tag spacedeck:0.1 idjohnson/spacedeck:0.1
builder@builder-T100:~/spacedeck-open$ docker push idjohnson/spacedeck:0.1
The push refers to repository [docker.io/idjohnson/spacedeck]
329089f994d9: Pushed
0663b90c631c: Pushed
605a5910c173: Pushed
732a9f505e99: Pushed
26b7725b6662: Pushed
c1226e3f2e76: Pushed
aae2f82b308d: Mounted from library/node
5003e477a3c5: Mounted from library/node
83ada9b02fd6: Mounted from library/node
75654b8eeebd: Mounted from library/node
0.1: digest: sha256:c33d126003c46e9f4915037fe66d0c61f56fdc477c5cead969efaea8f1f8d29f size: 2422

which you can find at https://hub.docker.com/r/idjohnson/spacedeck/tags

History

Spacedeck seemed far too complete to just be a hobby project. From the Github page, it was developed between 2011 and 2016 and was for a commercial SaaS that had been at http://spacedeck.com/ but was shuttered in 2018. The developers decided to open-source it for all in 2020.

I had to use the Wayback Machine to see it used to be just 5 Euro a month for a “Pro” account

/content/images/2024/12/spacedeck-11.png

However, it just sort of disappears in 2018 and nothing after. They left an index page in their Github that effectively says the same:

Spacedeck was a Berlin based SaaS business that provided web-based collaboration Spaces for individuals and teams. We closed our business in early 2018. The software lives on as an open source project at <a href="https://github.com/spacedeck">https://github.com/spacedeck</a>.

Summary

Today we looked at three very good options for self-hosted Open-Source whiteboards. We checked out Lorien, Whitebophir, and spacedeck-open.

Lorien was a fat binary that would run local. It works in a most basic sense and is quite fast - but without being able to write text, I cannot see how I would use this over MS Paint or whatever is local to my box.

The other two options are really quite close. Whitebophir is a simple container and a few of the options only show when double clicking. It has a straight anonymous options which is terrific for a fast whiteboard if in a meeting or consulting situation.

Spacedeck was clearly a polished commercial product first. I’m not as keen on having to create an account, but if one is trying to limit access, that could be a nice feature. I still have some issues with moving/editing items after creating them. It has to do with unclicking that pan option, but I believe with some practice this would be a good Miro alternative.

I’m leaving both open for others to try at whiteboard.freshbrewed.science (whitebophir) and spacedeck.freshbrewed.science (spacedeck-open). I have not changed the beta invite code, so just use “top-sekrit”.

OpenSource Kubernetes Docker Whiteboard Whitebophir Spacedeck Lorien

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes