Published: Aug 12, 2025 by Isaac Johnson
Today we’ll look at some apps from the DumbWare group. The first is Dumbpad which I first noted from a Marius post some time back. It’s a quick markdown gist sharing app.
Next we’ll checkout Dumbterm which will offer a very nice web-based terminal we can run in Docker or kubernetes.
Lastly, we’ll look at a simple but very elegant Homelab/Bookmark site (landing page), ISO.
Dumbpad
Back in February Marius posted about Dumbpad which bills itself as “a stupid simple, no auth, modern notepad with auto-save”.
Let’s start with a basic docker invokation
$ docker run -p 3443:3000 \
-v ./data:/app/data \
dumbwareio/dumbpad:latest
Which works just fine
It seems functional, albeit without Mermaid chart support
I noticed it works well for collaboration as the cursor shows other users logged in
I’ll add a title and a pin next
$ docker run -p 3443:3000 -e DUMBPAD_PIN=1234 -e SITE_TITLE="Happy Fun Zone" -v ./data:/app/data
dumbwareio/dumbpad:latest
> dumbpad@1.0.4 start
> node server.js
we are presented with a pin now at login
We can delete notes as one would expect
We can use light or dark mode and there are some settings like autos timing and showing remote connection messages (useful for presentations I imagine)
Let’s expose this, it might be useful at times.
I first need an A record
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n pad
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "6dce5d31-18c8-49bf-b1f2-5bad6eda4dc8",
"fqdn": "pad.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/pad",
"name": "pad",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"trafficManagementProfile": {},
"type": "Microsoft.Network/dnszones/A"
}
I don’t need to use Docker as we can easily pivot to K8s
$ cat manifest.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dumbpad-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dumbpad
spec:
replicas: 1
selector:
matchLabels:
app: dumbpad
template:
metadata:
labels:
app: dumbpad
spec:
containers:
- name: dumbpad
image: dumbwareio/dumbpad:latest
ports:
- containerPort: 3000
env:
- name: DUMBPAD_PIN
value: "1234"
- name: SITE_TITLE
value: "Happy Fun Zone"
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: dumbpad-data
---
apiVersion: v1
kind: Service
metadata:
name: dumbpad
spec:
type: ClusterIP
selector:
app: dumbpad
ports:
- protocol: TCP
port: 80
targetPort: 3000
$ kubectl apply -f ./manifest.yaml
persistentvolumeclaim/dumbpad-data created
deployment.apps/dumbpad created
service/dumbpad created
I can see the pod running
$ kubectl get po | grep dumb
dumbpad-f8585b46-crsq8 1/1 Running 0 46s
I can now create a quick ingress to use that A record
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
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: dumbpad
name: dumbpad-ingress
spec:
rules:
- host: pad.tpk.pw
http:
paths:
- backend:
service:
name: dumbpad
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- pad.tpk.pw
secretName: dumbpad-tls
I can the apply to create it
$ kubectl apply -f ./manifest.yaml
persistentvolumeclaim/dumbpad-data unchanged
deployment.apps/dumbpad unchanged
service/dumbpad unchanged
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/dumbpad-ingress created
When I see the cert created
$ kubectl get cert dumbpad-tls
NAME READY SECRET AGE
dumbpad-tls True dumbpad-tls 86s
And it works just fine with the pin 1234
DumbTerm
That actually got me excited about looking at other apps from Dumbware IO.
The first one I want to try is a Terminal app that could be handy for creating a simple backdoor back into our cluster.
The docker compose is
services:
dumbterm:
image: dumbwareio/dumbterm:latest
container_name: dumbterm
restart: unless-stopped
ports:
- ${DUMBTERM_PORT:-3000}:3000
volumes:
- ${DUMBTERM_CONFIG:-./config}:/root/.config
- ${DUMBTERM_DATA_DIR:-./data}:/root/data
environment:
# Container timezone
TZ: ${DUMBTERM_TZ:-America/Los_Angeles}
# The title shown in the web interface
SITE_TITLE: ${DUMBTERM_SITE_TITLE:-DumbTerm}
# Recommended PIN protection (leave empty to disable)
DUMBTERM_PIN: ${DUMBTERM_PIN:-1234}
# The base URL for the application
BASE_URL: ${DUMBTERM_BASE_URL:-http://localhost:3000}
ENABLE_STARSHIP: ${ENABLE_STARSHIP:-true}
LOCKOUT_TIME: ${DUMBTERM_LOCKOUT_TIME:-15} # Minutes
# Session duration in hours before requiring re-authentication
MAX_SESSION_AGE: ${DUMBTERM_MAX_SESSION_AGE:-24} # Hours
# (OPTIONAL) - List of allowed origins for CORS
# ALLOWED_ORIGINS: ${DUMBTERM_ALLOWED_ORIGINS:-http://localhost:3000}
I think we can pull that into a Kubernetes YAM manifest as before
$ cat term.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dumbterm-config
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dumbterm-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dumbterm
spec:
replicas: 1
selector:
matchLabels:
app: dumbterm
template:
metadata:
labels:
app: dumbterm
spec:
containers:
- name: dumbterm
image: dumbwareio/dumbterm:latest
ports:
- containerPort: 3000
env:
- name: TZ
value: "America/Chicago"
- name: SITE_TITLE
value: "DumbTerm"
- name: DUMBTERM_PIN
value: "1234"
- name: BASE_URL
value: "dumbterm.tpk.pw"
- name: ENABLE_STARSHIP
value: "true"
- name: LOCKOUT_TIME
value: "15"
- name: MAX_SESSION_AGE
value: "24"
volumeMounts:
- name: config
mountPath: /root/.config
- name: data
mountPath: /root/data
volumes:
- name: config
persistentVolumeClaim:
claimName: dumbterm-config
- name: data
persistentVolumeClaim:
claimName: dumbterm-data
---
apiVersion: v1
kind: Service
metadata:
name: dumbterm
spec:
type: ClusterIP
selector:
app: dumbterm
ports:
- protocol: TCP
port: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
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: dumbterm
name: dumbterm-ingress
spec:
rules:
- host: dumbterm.tpk.pw
http:
paths:
- backend:
service:
name: dumbterm
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- dumbterm.tpk.pw
secretName: dumbterm-tls
I just need to first create the A record
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n dumbterm
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "a11915d5-d5b6-4d92-bac1-32b9fbdb2caa",
"fqdn": "dumbterm.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/dumbterm",
"name": "dumbterm",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"trafficManagementProfile": {},
"type": "Microsoft.Network/dnszones/A"
}
Then apply
$ kubectl apply -f ./term.yaml
persistentvolumeclaim/dumbterm-config created
persistentvolumeclaim/dumbterm-data created
deployment.apps/dumbterm created
service/dumbterm created
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/dumbterm-ingress created
Once I see the cert is ready
$ kubectl get cert dumbterm-tls
NAME READY SECRET AGE
dumbterm-tls False dumbterm-tls 29s
$ kubectl get cert dumbterm-tls
NAME READY SECRET AGE
dumbterm-tls True dumbterm-tls 2m3s
I can test… looks like something went wrong
The pod is crashing and the logs suggest my URL is not right
$ kubectl logs dumbterm-76b886c644-gg8ml
Starship initialization completed!
> dumbterm@1.1.1 start
> npm run copy-xterm && node server.js
> dumbterm@1.1.1 copy-xterm
> node scripts/copy-xterm.js
Starting to copy xterm files...
Successfully copied all xterm files
node:internal/url:806
const href = bindingUrl.parse(input, base, raiseException);
^
TypeError: Invalid URL
at new URL (node:internal/url:806:29)
at Object.<anonymous> (/app/scripts/pwa-manifest-generator.js:5:42)
at Module._compile (node:internal/modules/cjs/loader:1529:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1613:10)
at Module.load (node:internal/modules/cjs/loader:1275:32)
at Module._load (node:internal/modules/cjs/loader:1096:12)
at Module.require (node:internal/modules/cjs/loader:1298:19)
at require (node:internal/modules/helpers:182:18)
at Object.<anonymous> (/app/server.js:12:33)
at Module._compile (node:internal/modules/cjs/loader:1529:14) {
code: 'ERR_INVALID_URL',
input: 'dumbterm.tpk.pw'
}
Node.js v20.19.2
npm notice
npm notice New major version of npm available! 10.8.2 -> 11.5.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.5.2
I’ll try using https://dumbterm.tpk.pw
instead and reapplying the manifest
$ kubectl apply -f term.yaml
persistentvolumeclaim/dumbterm-config unchanged
persistentvolumeclaim/dumbterm-data unchanged
deployment.apps/dumbterm configured
service/dumbterm unchanged
ingress.networking.k8s.io/dumbterm-ingress unchanged
That worked! And after entering a PIN, it seems launch just fine
I’ll try using it and installing a more graphical terminal app like btop
Even though we can share a container, we do not share sessions
I was about to say that a 4 digit PIN would just not suffice, but I tried kicking it up to a lot more and it worked
While using letters wont crash the pod
- name: DUMBTERM_PIN
value: "Password1"
The JavaScript login page will only accept numbers so there is no way to type it in.
However, even on pod restart, I can see where left off in my session
ISO dashboard
A few weeks ago I came across ISO which is a nice Open-Source home lab dashboard by Tim Ritter
Let’s give it a try locally first
$ docker run -d \
-p 3000:3000 \
> --name iso \
> -p 3000:3000 \
> -e AUTH_SECRET="changeme" \
> -e AUTH_PASSWORD="changeme" \
> -v ./config:/config \
> coyann/iso
Unable to find image 'coyann/iso:latest' locally
latest: Pulling from coyann/iso
c19952135643: Pull complete
159d19db919f: Pull complete
b9102aedc8d2: Pull complete
395992a1c37a: Pull complete
a66ada507b72: Pull complete
c5e91758c40d: Pull complete
83194e48f987: Pull complete
28d7da746fa2: Pull complete
2a20028bb2dd: Pull complete
ea2035f268a1: Pull complete
f36b5c28d7a4: Pull complete
e63215e643e4: Pull complete
986ad2432f4c: Pull complete
Digest: sha256:d0bb9816edb7a9c6658c597d30272fcd802caf9816b1fca2846b750e479f9c6a
Status: Downloaded newer image for coyann/iso:latest
ce51fc15633f046934a54babb3fc4fef8e1ca6b30318f56cc6616b3397a57036
It is certainly good looking
We have to login and use “changeme” to change any settings
Edit lets me just remove the icons. Then I can add them back with the URL I desire
I only put in a handful, but it would be a nice dashboard to use - feels very legacy iMac to me.
I’m not certain I’m ready to make a big landing page for global sharing, however.
That said, for those that might want to fire this up in Kubernetes:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: iso-config
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: iso
spec:
replicas: 1
selector:
matchLabels:
app: iso
template:
metadata:
labels:
app: iso
spec:
containers:
- name: iso
image: coyann/iso:latest
ports:
- containerPort: 3000
env:
- name: AUTH_SECRET
value: "changeme"
- name: AUTH_PASSWORD
value: "changeme"
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
persistentVolumeClaim:
claimName: iso-config
---
apiVersion: v1
kind: Service
metadata:
name: iso
spec:
type: ClusterIP
selector:
app: iso
ports:
- protocol: TCP
port: 80
targetPort:
Summary
Today we looked at a couple excellent Open-Source apps from the folks at Dumbware.io. The first was Dumbpad which was easy to launch in Docker and Kubernetes.
We can see it fired up at https://pad.tpk.pw/ with password 1234.
The other we explored was Dumbterm which creates a terminal pod in our cluster. Since this could be used to do some rather bad things, I did leave a private PIN on my deployed version. That said, you can use the pin 1234 on their demo version at https://dumbterm.dumbware.io/.
They have a lot more to check out some time like Dumbdo and Dumbkan amongst others.
The other app we took a look at was ISO which is a very elegant small HomeLab dashboard app. Though, I would argue this can just be a nice bookmark site for anyone to use - you can really use any URL you want on those icons. However, I’m still not certain I want to create a public ISO dashboard just yet.
Btw, I have to give credit to Copilot for nailing the ask for my background image.
In case you want the full size: