Add dashwise and tududi
Two self-hosted apps behind traefik, both keeping their state on the GlusterFS volume that is mounted on every node: - dashwise (home.wittyoneoff.com) is an all-in-one image running its web server, a bundled PocketBase and valkey side by side. Only port 3000 is published: the frontend resolves its backend as window.location.origin and every PocketBase call is made server-side, so 8090 stays inside the pod. It is on the service so the PocketBase admin UI can be reached with kubectl port-forward. - tududi (tududi.wittyoneoff.com) stores a SQLite database and user uploads. Both are VOLUMEs in the image, so both are backed by the claim -- as one volume mounted twice with subPath, because naming the same claim as two volume entries wedges kubelet, which is what happened with limesurvey. Both images are pinned to tags that publish an arm64 manifest. dashwise :latest and :stable are amd64 only and would not start on these nodes. Namespaces and secrets are applied out of band, as with the other apps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Dashwise all-in-one: the container runs the web/API server (3000), a bundled
|
||||
# PocketBase (8090) and valkey, all on localhost inside the pod.
|
||||
#
|
||||
# Only 3000 is exposed. The browser never talks to PocketBase directly -- the
|
||||
# frontend resolves its backend as window.location.origin and every
|
||||
# PocketBase call is made server-side -- so 8090 stays internal.
|
||||
#
|
||||
# NOTE: :latest and :stable are built for amd64 ONLY and will not run on this
|
||||
# arm64 cluster. Use a tag that publishes an arm64 manifest.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dashwise
|
||||
namespace: dashwise
|
||||
labels:
|
||||
app.kubernetes.io/name: dashwise
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
# the PV is ReadWriteOnce and PocketBase is a single-writer SQLite store,
|
||||
# so the old pod has to go before the new one starts
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: dashwise
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: dashwise
|
||||
spec:
|
||||
containers:
|
||||
- name: dashwise
|
||||
image: andreasmolnardev/dashwise:v1.0-rc1
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: ENVIRONMENT
|
||||
value: production
|
||||
- name: INSTANCE_NAME
|
||||
value: Witty One Off
|
||||
- name: START_POCKETBASE
|
||||
value: "true"
|
||||
- name: PB_BINARY_PATH
|
||||
value: /usr/local/bin/pocketbase
|
||||
# in-pod addresses: the server talks to PocketBase over localhost
|
||||
- name: PB_URL
|
||||
value: http://127.0.0.1:8090
|
||||
- name: NEXT_PUBLIC_PB_URL
|
||||
value: http://127.0.0.1:8090
|
||||
- name: APP_BASE_URL
|
||||
value: https://home.wittyoneoff.com
|
||||
- name: NEXT_PUBLIC_APP_URL
|
||||
value: https://home.wittyoneoff.com
|
||||
- name: PB_ADMIN_EMAIL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dashwise
|
||||
key: pb-admin-email
|
||||
- name: PB_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dashwise
|
||||
key: pb-admin-password
|
||||
- name: TZ
|
||||
value: America/Los_Angeles
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
- name: pocketbase
|
||||
containerPort: 8090
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
failureThreshold: 60
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/pocketbase/pb_data
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: dashwise
|
||||
@@ -0,0 +1,23 @@
|
||||
# Both hostnames point at the same backend: the public one used by the EC2
|
||||
# nginx-proxy-manager, and the origin-* alias traefik matches on when NPM
|
||||
# forwards to origin.wittyoneoff.com:8080.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dashwise
|
||||
namespace: dashwise
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: home.wittyoneoff.com
|
||||
http: &backend
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: dashwise
|
||||
port:
|
||||
number: 3000
|
||||
- host: origin-home.wittyoneoff.com
|
||||
http: *backend
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
# secret.example.yaml is intentionally NOT listed -- copy it to secret.yaml
|
||||
# (gitignored), set real values, and apply it once per cluster.
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- storage.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: dashwise
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copy to secret.yaml (gitignored), set real values, and apply once:
|
||||
# kubectl apply -f secret.yaml
|
||||
# These are the PocketBase superuser credentials dashwise creates on first
|
||||
# start; they are also what you use to reach the PocketBase admin UI.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dashwise
|
||||
namespace: dashwise
|
||||
type: Opaque
|
||||
stringData:
|
||||
pb-admin-email: you@example.com
|
||||
pb-admin-password: CHANGE-ME
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dashwise
|
||||
namespace: dashwise
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: dashwise
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: http
|
||||
# PocketBase is deliberately NOT published through the ingress; this port
|
||||
# exists so its admin UI can be reached with `kubectl port-forward`.
|
||||
- name: pocketbase
|
||||
port: 8090
|
||||
targetPort: pocketbase
|
||||
@@ -0,0 +1,29 @@
|
||||
# PocketBase data lives on GlusterFS (/mnt/gvolume0 is mounted on every node),
|
||||
# so the pod can be scheduled on any of them.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: dashwise-pv
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /mnt/gvolume0/dashwise/pb_data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: dashwise
|
||||
namespace: dashwise
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
storageClassName: ""
|
||||
volumeName: dashwise-pv
|
||||
@@ -0,0 +1,80 @@
|
||||
# tududi keeps everything in a SQLite file under /app/db and user uploads
|
||||
# under /app/uploads. Both are declared VOLUMEs in the image, so both have to
|
||||
# be backed by the PVC or they are lost on restart.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: tududi
|
||||
namespace: tududi
|
||||
labels:
|
||||
app.kubernetes.io/name: tududi
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
# single-writer SQLite on a ReadWriteOnce volume
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: tududi
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: tududi
|
||||
spec:
|
||||
containers:
|
||||
- name: tududi
|
||||
image: chrisvel/tududi:1.3.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
# tududi sits behind traefik and the EC2 proxy, so it has to trust
|
||||
# X-Forwarded-* to build correct URLs and mark cookies secure
|
||||
- name: TUDUDI_TRUST_PROXY
|
||||
value: "true"
|
||||
- name: TUDUDI_USER_EMAIL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tududi
|
||||
key: user-email
|
||||
- name: TUDUDI_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tududi
|
||||
key: user-password
|
||||
- name: TUDUDI_SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tududi
|
||||
key: session-secret
|
||||
- name: TZ
|
||||
value: America/Los_Angeles
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3002
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
failureThreshold: 60
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/db
|
||||
subPath: db
|
||||
- name: data
|
||||
mountPath: /app/uploads
|
||||
subPath: uploads
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: tududi
|
||||
@@ -0,0 +1,22 @@
|
||||
# Public hostname plus the origin-* alias traefik matches on when the EC2
|
||||
# nginx-proxy-manager forwards to origin.wittyoneoff.com:8080.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: tududi
|
||||
namespace: tududi
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: tududi.wittyoneoff.com
|
||||
http: &backend
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: tududi
|
||||
port:
|
||||
number: 3002
|
||||
- host: origin-tududi.wittyoneoff.com
|
||||
http: *backend
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
# secret.example.yaml is intentionally NOT listed -- copy it to secret.yaml
|
||||
# (gitignored), set real values, and apply it once per cluster.
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- storage.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: tududi
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copy to secret.yaml (gitignored), set real values, and apply once:
|
||||
# kubectl apply -f secret.yaml
|
||||
# user-email / user-password are the login tududi creates on first start.
|
||||
# session-secret signs session cookies -- generate with:
|
||||
# openssl rand -hex 64
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: tududi
|
||||
namespace: tududi
|
||||
type: Opaque
|
||||
stringData:
|
||||
user-email: you@example.com
|
||||
user-password: CHANGE-ME
|
||||
session-secret: CHANGE-ME
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: tududi
|
||||
namespace: tududi
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: tududi
|
||||
ports:
|
||||
- name: http
|
||||
port: 3002
|
||||
targetPort: http
|
||||
@@ -0,0 +1,31 @@
|
||||
# One volume for both of tududi's data paths, mounted twice with subPath.
|
||||
# Declaring the same PVC as two separate volume entries in one pod wedges
|
||||
# kubelet's volume setup (pod stuck in Init with no events) -- that is what
|
||||
# happened with limesurvey; do not split this into db-pv and uploads-pv.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: tududi-pv
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /mnt/gvolume0/tududi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: tududi
|
||||
namespace: tududi
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
storageClassName: ""
|
||||
volumeName: tududi-pv
|
||||
@@ -8,3 +8,5 @@ resources:
|
||||
- apps/pihole
|
||||
- apps/unbound
|
||||
- apps/unified-streaming
|
||||
- apps/dashwise
|
||||
- apps/tududi
|
||||
|
||||
Reference in New Issue
Block a user