From 24787d7bf7a73afddc638bea627e4163f8ad7241 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 19:10:03 +0000 Subject: [PATCH 01/10] added recipes --- recipes/recipes.yaml | 206 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 recipes/recipes.yaml diff --git a/recipes/recipes.yaml b/recipes/recipes.yaml new file mode 100644 index 0000000..515af36 --- /dev/null +++ b/recipes/recipes.yaml @@ -0,0 +1,206 @@ +# ====================================================== +# TANDOOR RECIPES - FULL STACK +# Domain: recipes.juntekim.com +# ====================================================== + +# ------------------------- +# POSTGRES PVC +# ------------------------- +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: tandoor-postgres-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: microk8s-hostpath + resources: + requests: + storage: 2Gi + +# ------------------------- +# MEDIA PVC +# ------------------------- +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: tandoor-media-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: microk8s-hostpath + resources: + requests: + storage: 5Gi + +# ------------------------- +# POSTGRES +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tandoor-postgres +spec: + replicas: 1 + selector: + matchLabels: + app: tandoor-postgres + template: + metadata: + labels: + app: tandoor-postgres + spec: + containers: + - name: postgres + image: postgres:15-alpine + env: + - name: POSTGRES_USER + value: tandoor + - name: POSTGRES_PASSWORD + value: tandoorpassword + - name: POSTGRES_DB + value: tandoor + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-storage + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: tandoor-postgres-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: tandoor-postgres +spec: + selector: + app: tandoor-postgres + ports: + - port: 5432 + +# ------------------------- +# REDIS +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tandoor-redis +spec: + replicas: 1 + selector: + matchLabels: + app: tandoor-redis + template: + metadata: + labels: + app: tandoor-redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + +--- +apiVersion: v1 +kind: Service +metadata: + name: tandoor-redis +spec: + selector: + app: tandoor-redis + ports: + - port: 6379 + +# ------------------------- +# TANDOOR APP +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tandoor +spec: + replicas: 1 + selector: + matchLabels: + app: tandoor + template: + metadata: + labels: + app: tandoor + spec: + containers: + - name: tandoor + image: vabene1111/recipes:latest + env: + - name: SECRET_KEY + value: replace-with-random-secret + - name: DB_ENGINE + value: django.db.backends.postgresql + - name: POSTGRES_HOST + value: tandoor-postgres + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: tandoor + - name: POSTGRES_PASSWORD + value: tandoorpassword + - name: POSTGRES_DB + value: tandoor + - name: REDIS_HOST + value: tandoor-redis + - name: REDIS_PORT + value: "6379" + - name: ALLOWED_HOSTS + value: recipes.juntekim.com + - name: DEBUG + value: "0" + ports: + - containerPort: 8080 + volumeMounts: + - name: media-storage + mountPath: /opt/recipes/mediafiles + volumes: + - name: media-storage + persistentVolumeClaim: + claimName: tandoor-media-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: tandoor +spec: + selector: + app: tandoor + ports: + - port: 8080 + targetPort: 8080 + +# ------------------------- +# TRAEFIK INGRESS +# ------------------------- +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: tandoor-ingress +spec: + entryPoints: + - websecure + routes: + - match: Host(`recipes.juntekim.com`) + kind: Rule + services: + - name: tandoor + port: 8080 + tls: + certResolver: myresolver + domains: + - main: recipes.juntekim.com \ No newline at end of file From 90e8920d6286bc3f17ece17859448c2700af0f8c Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 19:30:18 +0000 Subject: [PATCH 02/10] save recipes --- recipes/recipes.yaml | 82 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/recipes/recipes.yaml b/recipes/recipes.yaml index 515af36..fe61226 100644 --- a/recipes/recipes.yaml +++ b/recipes/recipes.yaml @@ -1,11 +1,33 @@ # ====================================================== -# TANDOOR RECIPES - FULL STACK -# Domain: recipes.juntekim.com +# TANDOOR RECIPES - PRODUCTION (PINNED TO MIST) # ====================================================== # ------------------------- -# POSTGRES PVC +# POSTGRES PV # ------------------------- +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: tandoor-postgres-pv +spec: + capacity: + storage: 2Gi + accessModes: + - ReadWriteOnce + storageClassName: tandoor-local-storage + persistentVolumeReclaimPolicy: Retain + local: + path: /home/kimjunte/k8s_storage/tandoor/postgres + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - mist + --- apiVersion: v1 kind: PersistentVolumeClaim @@ -14,14 +36,37 @@ metadata: spec: accessModes: - ReadWriteOnce - storageClassName: microk8s-hostpath + storageClassName: tandoor-local-storage resources: requests: storage: 2Gi # ------------------------- -# MEDIA PVC +# MEDIA PV # ------------------------- +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: tandoor-media-pv +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteOnce + storageClassName: tandoor-local-storage + persistentVolumeReclaimPolicy: Retain + local: + path: /home/kimjunte/k8s_storage/tandoor/media + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - mist + --- apiVersion: v1 kind: PersistentVolumeClaim @@ -30,7 +75,7 @@ metadata: spec: accessModes: - ReadWriteOnce - storageClassName: microk8s-hostpath + storageClassName: tandoor-local-storage resources: requests: storage: 5Gi @@ -43,6 +88,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor-postgres + labels: + app: tandoor-postgres spec: replicas: 1 selector: @@ -53,6 +100,8 @@ spec: labels: app: tandoor-postgres spec: + nodeSelector: + kubernetes.io/hostname: mist containers: - name: postgres image: postgres:15-alpine @@ -90,6 +139,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor-redis + labels: + app: tandoor-redis spec: replicas: 1 selector: @@ -100,6 +151,8 @@ spec: labels: app: tandoor-redis spec: + nodeSelector: + kubernetes.io/hostname: mist containers: - name: redis image: redis:7-alpine @@ -125,6 +178,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor + labels: + app: tandoor spec: replicas: 1 selector: @@ -135,12 +190,14 @@ spec: labels: app: tandoor spec: + nodeSelector: + kubernetes.io/hostname: mist containers: - name: tandoor image: vabene1111/recipes:latest env: - name: SECRET_KEY - value: replace-with-random-secret + value: replace-with-very-long-random-string - name: DB_ENGINE value: django.db.backends.postgresql - name: POSTGRES_HOST @@ -180,7 +237,7 @@ spec: selector: app: tandoor ports: - - port: 8080 + - port: 80 targetPort: 8080 # ------------------------- @@ -195,12 +252,11 @@ spec: entryPoints: - websecure routes: - - match: Host(`recipes.juntekim.com`) + - match: Host(`mealcraft.com`) kind: Rule services: - name: tandoor - port: 8080 + port: 80 + passHostHeader: true tls: - certResolver: myresolver - domains: - - main: recipes.juntekim.com \ No newline at end of file + certResolver: myresolver \ No newline at end of file From e5309c179411e33295e0d47d06bc468b9284462d Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 20:54:36 +0000 Subject: [PATCH 03/10] jspaint --- draw/draw.yaml | 45 ++++++++++++++++++++++---------------------- recipes/recipes.yaml | 20 ++++++++++++-------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/draw/draw.yaml b/draw/draw.yaml index fb4a523..1cd7b02 100644 --- a/draw/draw.yaml +++ b/draw/draw.yaml @@ -1,46 +1,46 @@ -# ================================ -# EXCALIDRAW - STATELESS -# https://excalidraw.com -# ================================ +# ========================================== +# JS PAINT (STATELESS) +# ========================================== --- apiVersion: apps/v1 kind: Deployment metadata: - name: excalidraw - labels: - app: excalidraw + name: jspaint spec: replicas: 1 selector: matchLabels: - app: excalidraw + app: jspaint template: metadata: labels: - app: excalidraw + app: jspaint spec: + nodeSelector: + kubernetes.io/hostname: mist + containers: - - name: excalidraw - image: excalidraw/excalidraw:latest + - name: jspaint + image: ghcr.io/1j01/jspaint:latest ports: - containerPort: 80 resources: requests: - cpu: "100m" - memory: "128Mi" + cpu: "50m" + memory: "64Mi" limits: - cpu: "300m" - memory: "256Mi" + cpu: "200m" + memory: "128Mi" --- apiVersion: v1 kind: Service metadata: - name: excalidraw + name: jspaint spec: selector: - app: excalidraw + app: jspaint ports: - port: 80 targetPort: 80 @@ -49,17 +49,16 @@ spec: apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: - name: excalidraw-ingressroute + name: jspaint-ingress spec: entryPoints: - websecure routes: - - match: Host(`draw.juntekim.com`) + - match: Host(`jspaint.juntekim.com`) kind: Rule services: - - name: excalidraw + - name: jspaint port: 80 + passHostHeader: true tls: - certResolver: myresolver - domains: - - main: draw.juntekim.com + certResolver: myresolver \ No newline at end of file diff --git a/recipes/recipes.yaml b/recipes/recipes.yaml index fe61226..86791e5 100644 --- a/recipes/recipes.yaml +++ b/recipes/recipes.yaml @@ -88,8 +88,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor-postgres - labels: - app: tandoor-postgres spec: replicas: 1 selector: @@ -139,8 +137,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor-redis - labels: - app: tandoor-redis spec: replicas: 1 selector: @@ -178,8 +174,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: tandoor - labels: - app: tandoor spec: replicas: 1 selector: @@ -192,12 +186,16 @@ spec: spec: nodeSelector: kubernetes.io/hostname: mist + + enableServiceLinks: false # 🔥 CRITICAL FIX + containers: - name: tandoor image: vabene1111/recipes:latest env: - name: SECRET_KEY - value: replace-with-very-long-random-string + value: replace-with-long-random-string + - name: DB_ENGINE value: django.db.backends.postgresql - name: POSTGRES_HOST @@ -210,19 +208,25 @@ spec: value: tandoorpassword - name: POSTGRES_DB value: tandoor + - name: REDIS_HOST value: tandoor-redis - name: REDIS_PORT value: "6379" + - name: ALLOWED_HOSTS - value: recipes.juntekim.com + value: "*" + - name: DEBUG value: "0" + ports: - containerPort: 8080 + volumeMounts: - name: media-storage mountPath: /opt/recipes/mediafiles + volumes: - name: media-storage persistentVolumeClaim: From e9148bc1af9306181a5462b3a614338ce31d54d4 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 21:17:07 +0000 Subject: [PATCH 04/10] build --- draw/draw.yaml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/draw/draw.yaml b/draw/draw.yaml index 1cd7b02..4d3692f 100644 --- a/draw/draw.yaml +++ b/draw/draw.yaml @@ -1,5 +1,5 @@ # ========================================== -# JS PAINT (STATELESS) +# JS PAINT (STATIC DEPLOYMENT) # ========================================== --- @@ -20,11 +20,31 @@ spec: nodeSelector: kubernetes.io/hostname: mist + volumes: + - name: web-content + emptyDir: {} + + initContainers: + - name: fetch-jspaint + image: alpine/git + command: + - sh + - -c + - | + git clone --depth=1 https://github.com/1j01/jspaint.git /tmp/jspaint && \ + cp -r /tmp/jspaint/* /web + volumeMounts: + - name: web-content + mountPath: /web + containers: - - name: jspaint - image: ghcr.io/1j01/jspaint:latest + - name: nginx + image: nginx:alpine ports: - containerPort: 80 + volumeMounts: + - name: web-content + mountPath: /usr/share/nginx/html resources: requests: cpu: "50m" From 93341e914276090a0d19360609b99354e11617c9 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 21:41:29 +0000 Subject: [PATCH 05/10] save receipes --- recipes/recipes.yaml | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/recipes/recipes.yaml b/recipes/recipes.yaml index 86791e5..d269d36 100644 --- a/recipes/recipes.yaml +++ b/recipes/recipes.yaml @@ -110,6 +110,18 @@ spec: value: tandoorpassword - name: POSTGRES_DB value: tandoor + - name: SITE_URL + value: https://mealcraft.com + - name: ALLOWED_HOSTS + value: mealcraft.com + - name: CSRF_TRUSTED_ORIGINS + value: https://mealcraft.com + - name: NGINX_PROXY + value: "1" + - name: DEBUG + value: "1" + - name: SECURE_PROXY_SSL_HEADER + value: HTTP_X_FORWARDED_PROTO,https volumeMounts: - mountPath: /var/lib/postgresql/data name: postgres-storage @@ -191,31 +203,37 @@ spec: containers: - name: tandoor - image: vabene1111/recipes:latest + image: vabene1111/recipes:1.5.24 + env: - name: SECRET_KEY value: replace-with-long-random-string - name: DB_ENGINE value: django.db.backends.postgresql + - name: POSTGRES_HOST value: tandoor-postgres - name: POSTGRES_PORT value: "5432" + - name: POSTGRES_DB + value: tandoor - name: POSTGRES_USER value: tandoor - name: POSTGRES_PASSWORD value: tandoorpassword - - name: POSTGRES_DB - value: tandoor - - name: REDIS_HOST - value: tandoor-redis - - name: REDIS_PORT - value: "6379" + - name: REDIS_URL + value: redis://tandoor-redis:6379/0 - name: ALLOWED_HOSTS - value: "*" + value: mealcraft.com + + - name: CSRF_TRUSTED_ORIGINS + value: https://mealcraft.com + + - name: NGINX_PROXY + value: "1" - name: DEBUG value: "0" From a27b90e39c7bab7650b4dff0b272d160893b9301 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 23 Feb 2026 21:58:54 +0000 Subject: [PATCH 06/10] save --- recipes/recipes.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/recipes/recipes.yaml b/recipes/recipes.yaml index d269d36..2852255 100644 --- a/recipes/recipes.yaml +++ b/recipes/recipes.yaml @@ -211,7 +211,8 @@ spec: - name: DB_ENGINE value: django.db.backends.postgresql - + - name: GUNICORN_MEDIA + value: "1" - name: POSTGRES_HOST value: tandoor-postgres - name: POSTGRES_PORT From 47dcffc97a28b37dab66c347955df18a5ea43320 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 24 Feb 2026 05:29:01 +0000 Subject: [PATCH 07/10] got to exercise --- exercise/exercise.yaml | 248 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 exercise/exercise.yaml diff --git a/exercise/exercise.yaml b/exercise/exercise.yaml new file mode 100644 index 0000000..e2c9b43 --- /dev/null +++ b/exercise/exercise.yaml @@ -0,0 +1,248 @@ +# ====================================================== +# WGER - PRODUCTION (exercise.juntekim.com) +# ====================================================== + +# ------------------------- +# POSTGRES PV +# ------------------------- +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: wger-postgres-pv +spec: + capacity: + storage: 2Gi + accessModes: + - ReadWriteOnce + storageClassName: wger-local-storage + persistentVolumeReclaimPolicy: Retain + local: + path: /home/kimjunte/k8s_storage/wger/postgres + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - mist + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wger-postgres-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: wger-local-storage + resources: + requests: + storage: 2Gi + +# ------------------------- +# MEDIA PV +# ------------------------- +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: wger-media-pv +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteOnce + storageClassName: wger-local-storage + persistentVolumeReclaimPolicy: Retain + local: + path: /home/kimjunte/k8s_storage/wger/media + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - mist + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wger-media-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: wger-local-storage + resources: + requests: + storage: 5Gi + +# ------------------------- +# POSTGRES +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wger-postgres +spec: + replicas: 1 + selector: + matchLabels: + app: wger-postgres + template: + metadata: + labels: + app: wger-postgres + spec: + nodeSelector: + kubernetes.io/hostname: mist + containers: + - name: postgres + image: postgres:15-alpine + env: + - name: POSTGRES_USER + value: wger + - name: POSTGRES_PASSWORD + value: wgerpassword + - name: POSTGRES_DB + value: wger + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-storage + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: wger-postgres-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: wger-postgres +spec: + selector: + app: wger-postgres + ports: + - port: 5432 + +# ------------------------- +# REDIS +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wger-redis +spec: + replicas: 1 + selector: + matchLabels: + app: wger-redis + template: + metadata: + labels: + app: wger-redis + spec: + nodeSelector: + kubernetes.io/hostname: mist + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + +--- +apiVersion: v1 +kind: Service +metadata: + name: wger-redis +spec: + selector: + app: wger-redis + ports: + - port: 6379 + +# ------------------------- +# WGER APP +# ------------------------- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wger +spec: + replicas: 1 + selector: + matchLabels: + app: wger + template: + metadata: + labels: + app: wger + spec: + nodeSelector: + kubernetes.io/hostname: mist + containers: + - name: wger + image: wger/server:latest + env: + - name: DJANGO_SECRET_KEY + value: replace-with-long-random-string + + - name: DATABASE_URL + value: postgres://wger:wgerpassword@wger-postgres:5432/wger + + - name: CACHE_URL + value: redis://wger-redis:6379/1 + + - name: ALLOWED_HOSTS + value: exercise.juntekim.com + + ports: + - containerPort: 8000 + + volumeMounts: + - name: media-storage + mountPath: /home/wger/media + + volumes: + - name: media-storage + persistentVolumeClaim: + claimName: wger-media-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: wger +spec: + selector: + app: wger + ports: + - port: 80 + targetPort: 8000 + +# ------------------------- +# TRAEFIK INGRESS +# ------------------------- +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: wger-ingress +spec: + entryPoints: + - websecure + routes: + - match: Host(`exercise.juntekim.com`) + kind: Rule + services: + - name: wger + port: 80 + passHostHeader: true + tls: + certResolver: myresolver \ No newline at end of file From 0465146fecb786b573011ab6183f294fac1092bf Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 24 Feb 2026 06:33:27 +0000 Subject: [PATCH 08/10] got to exercise --- exercise/exercise.yaml | 201 +++++++++++++++++++++++++++++++++-------- 1 file changed, 164 insertions(+), 37 deletions(-) diff --git a/exercise/exercise.yaml b/exercise/exercise.yaml index e2c9b43..e89501f 100644 --- a/exercise/exercise.yaml +++ b/exercise/exercise.yaml @@ -1,5 +1,21 @@ # ====================================================== -# WGER - PRODUCTION (exercise.juntekim.com) +# WGER - PRODUCTION ARCHITECTURE +# Traefik → nginx → wger → postgres +# ====================================================== + +# ------------------------- +# STORAGE CLASS +# ------------------------- +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: wger-local-storage +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: WaitForFirstConsumer + +# ====================================================== +# PERSISTENT VOLUMES # ====================================================== # ------------------------- @@ -13,8 +29,7 @@ metadata: spec: capacity: storage: 2Gi - accessModes: - - ReadWriteOnce + accessModes: [ReadWriteOnce] storageClassName: wger-local-storage persistentVolumeReclaimPolicy: Retain local: @@ -25,17 +40,49 @@ spec: - matchExpressions: - key: kubernetes.io/hostname operator: In - values: - - mist - + values: [mist] --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: wger-postgres-pvc spec: - accessModes: - - ReadWriteOnce + accessModes: [ReadWriteOnce] + storageClassName: wger-local-storage + resources: + requests: + storage: 2Gi + +# ------------------------- +# STATIC PV +# ------------------------- +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: wger-static-pv +spec: + capacity: + storage: 2Gi + accessModes: [ReadWriteOnce] + storageClassName: wger-local-storage + persistentVolumeReclaimPolicy: Retain + local: + path: /home/kimjunte/k8s_storage/wger/static + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [mist] +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wger-static-pvc +spec: + accessModes: [ReadWriteOnce] storageClassName: wger-local-storage resources: requests: @@ -52,8 +99,7 @@ metadata: spec: capacity: storage: 5Gi - accessModes: - - ReadWriteOnce + accessModes: [ReadWriteOnce] storageClassName: wger-local-storage persistentVolumeReclaimPolicy: Retain local: @@ -64,25 +110,23 @@ spec: - matchExpressions: - key: kubernetes.io/hostname operator: In - values: - - mist - + values: [mist] --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: wger-media-pvc spec: - accessModes: - - ReadWriteOnce + accessModes: [ReadWriteOnce] storageClassName: wger-local-storage resources: requests: storage: 5Gi -# ------------------------- +# ====================================================== # POSTGRES -# ------------------------- +# ====================================================== + --- apiVersion: apps/v1 kind: Deployment @@ -129,9 +173,10 @@ spec: ports: - port: 5432 -# ------------------------- +# ====================================================== # REDIS -# ------------------------- +# ====================================================== + --- apiVersion: apps/v1 kind: Deployment @@ -152,8 +197,6 @@ spec: containers: - name: redis image: redis:7-alpine - ports: - - containerPort: 6379 --- apiVersion: v1 @@ -166,9 +209,10 @@ spec: ports: - port: 6379 -# ------------------------- +# ====================================================== # WGER APP -# ------------------------- +# ====================================================== + --- apiVersion: apps/v1 kind: Deployment @@ -190,26 +234,27 @@ spec: - name: wger image: wger/server:latest env: - - name: DJANGO_SECRET_KEY - value: replace-with-long-random-string - - name: DATABASE_URL value: postgres://wger:wgerpassword@wger-postgres:5432/wger - - name: CACHE_URL value: redis://wger-redis:6379/1 - + - name: DJANGO_SECRET_KEY + value: replace-with-long-random-string - name: ALLOWED_HOSTS - value: exercise.juntekim.com - + value: "*" + - name: CSRF_TRUSTED_ORIGINS + value: https://exercise.juntekim.com ports: - containerPort: 8000 - volumeMounts: + - name: static-storage + mountPath: /home/wger/static - name: media-storage mountPath: /home/wger/media - volumes: + - name: static-storage + persistentVolumeClaim: + claimName: wger-static-pvc - name: media-storage persistentVolumeClaim: claimName: wger-media-pvc @@ -223,12 +268,95 @@ spec: selector: app: wger ports: - - port: 80 + - port: 8000 targetPort: 8000 -# ------------------------- +# ====================================================== +# NGINX (STATIC + PROXY) +# ====================================================== + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: wger-nginx-config +data: + default.conf: | + server { + listen 80; + + location /static/ { + alias /home/wger/static/; + } + + location /media/ { + alias /home/wger/media/; + } + + location / { + proxy_pass http://wger:8000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wger-nginx +spec: + replicas: 1 + selector: + matchLabels: + app: wger-nginx + template: + metadata: + labels: + app: wger-nginx + spec: + nodeSelector: + kubernetes.io/hostname: mist + containers: + - name: nginx + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: static-storage + mountPath: /home/wger/static + - name: media-storage + mountPath: /home/wger/media + - name: nginx-config + mountPath: /etc/nginx/conf.d + volumes: + - name: static-storage + persistentVolumeClaim: + claimName: wger-static-pvc + - name: media-storage + persistentVolumeClaim: + claimName: wger-media-pvc + - name: nginx-config + configMap: + name: wger-nginx-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: wger-nginx +spec: + selector: + app: wger-nginx + ports: + - port: 80 + targetPort: 80 + +# ====================================================== # TRAEFIK INGRESS -# ------------------------- +# ====================================================== + --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute @@ -241,8 +369,7 @@ spec: - match: Host(`exercise.juntekim.com`) kind: Rule services: - - name: wger + - name: wger-nginx port: 80 - passHostHeader: true tls: certResolver: myresolver \ No newline at end of file From 5b188f0b320f29d55342a5f31685ea23f09ce008 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sat, 28 Feb 2026 23:03:03 +0000 Subject: [PATCH 09/10] add server --- .devcontainer/stripe-to-invoice/Dockerfile | 5 +- .../stripe-to-invoice/devcontainer.json | 5 +- .../stripe-to-invoice/docker-compose.yml | 7 ++- .../stripe-to-invoice/post-install.sh | 2 +- code-server/codeserver.yaml | 62 +++++++++++++++++++ 5 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 code-server/codeserver.yaml diff --git a/.devcontainer/stripe-to-invoice/Dockerfile b/.devcontainer/stripe-to-invoice/Dockerfile index 554dc1a..ec3a134 100644 --- a/.devcontainer/stripe-to-invoice/Dockerfile +++ b/.devcontainer/stripe-to-invoice/Dockerfile @@ -51,9 +51,8 @@ RUN echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe RUN sudo apt update RUN sudo apt install stripe - - - +# Install code server +RUN curl -fsSL https://code-server.dev/install.sh | sh # Set the working directory WORKDIR /workspaces/monorepo \ No newline at end of file diff --git a/.devcontainer/stripe-to-invoice/devcontainer.json b/.devcontainer/stripe-to-invoice/devcontainer.json index d8ae29d..f35aa4f 100644 --- a/.devcontainer/stripe-to-invoice/devcontainer.json +++ b/.devcontainer/stripe-to-invoice/devcontainer.json @@ -2,9 +2,10 @@ "name": "Basic Python", "dockerComposeFile": "docker-compose.yml", "service": "one_repo_to_rule_them_all", - "remoteUser": "vscode", + // "remoteUser": "vscode", "workspaceFolder": "/workspaces/monorepo", - "postStartCommand": "bash .devcontainer/post-install.sh", + "postStartCommand": "bash .devcontainer/stripe-to-invoice/post-install.sh", + "forwardPorts": [8080], "features": { // "ghcr.io/devcontainers/features/ssh-agent:1": {} diff --git a/.devcontainer/stripe-to-invoice/docker-compose.yml b/.devcontainer/stripe-to-invoice/docker-compose.yml index f9d12d9..42daecc 100644 --- a/.devcontainer/stripe-to-invoice/docker-compose.yml +++ b/.devcontainer/stripe-to-invoice/docker-compose.yml @@ -2,12 +2,15 @@ version: '3.8' services: one_repo_to_rule_them_all: - user: "${UID}:${GID}" + # user: "${UID}:${GID}" build: context: ../.. dockerfile: .devcontainer/stripe-to-invoice/Dockerfile - command: sleep infinity + command: code-server --bind-addr 0.0.0.0:8080 --auth password + # command: sleep infinity volumes: - ../..:/workspaces/monorepo extra_hosts: - "host.docker.internal:host-gateway" + ports: + - "8080:8080" diff --git a/.devcontainer/stripe-to-invoice/post-install.sh b/.devcontainer/stripe-to-invoice/post-install.sh index 482fcc1..d0d7dd0 100644 --- a/.devcontainer/stripe-to-invoice/post-install.sh +++ b/.devcontainer/stripe-to-invoice/post-install.sh @@ -1 +1 @@ -cv stripe_to_invoice && npm install; \ No newline at end of file +cd stripe_to_invoice && npm install; \ No newline at end of file diff --git a/code-server/codeserver.yaml b/code-server/codeserver.yaml new file mode 100644 index 0000000..d3646d3 --- /dev/null +++ b/code-server/codeserver.yaml @@ -0,0 +1,62 @@ +# ====================================================== +# DEV.JUNTEKIM.COM → LOCALHOST:8080 +# ====================================================== + +--- +apiVersion: v1 +kind: Service +metadata: + name: dev-juntekim-external +spec: + type: ExternalName + externalName: host.docker.internal # change if needed + ports: + - port: 8080 + +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: dev-juntekim-external +subsets: + - addresses: + - ip: 192.168.0.181 + ports: + - port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: dev-juntekim-service +spec: + ports: + - port: 80 + targetPort: 8080 + selector: {} # no selector — used with external endpoints + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dev-juntekim-ingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: myresolver +spec: + ingressClassName: traefik + tls: + - hosts: + - dev.juntekim.com + secretName: dev-juntekim-tls + rules: + - host: dev.juntekim.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dev-juntekim-service + port: + number: 80 \ No newline at end of file From 71ea3e4da482a13bda47ac55af70475c09541cc4 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sun, 1 Mar 2026 17:18:30 +0000 Subject: [PATCH 10/10] stripe to invoice --- .../stripe-to-invoice/devcontainer.json | 2 +- .../stripe-to-invoice/docker-compose.yml | 3 +- code-server/codeserver.yaml | 69 ++--- code-server/sal-codeservery.yaml | 34 +++ stripe_to_invoice/README.md | 248 ------------------ 5 files changed, 58 insertions(+), 298 deletions(-) create mode 100644 code-server/sal-codeservery.yaml delete mode 100644 stripe_to_invoice/README.md diff --git a/.devcontainer/stripe-to-invoice/devcontainer.json b/.devcontainer/stripe-to-invoice/devcontainer.json index f35aa4f..ca42b31 100644 --- a/.devcontainer/stripe-to-invoice/devcontainer.json +++ b/.devcontainer/stripe-to-invoice/devcontainer.json @@ -13,7 +13,7 @@ "mounts": [ // Optional convenience mount - "source=${localEnv:HOME},target=/workspaces/home,type=bind" + "source=${localEnv:HOME},target=/home/vscode,type=bind" ], "customizations": { diff --git a/.devcontainer/stripe-to-invoice/docker-compose.yml b/.devcontainer/stripe-to-invoice/docker-compose.yml index 42daecc..91264df 100644 --- a/.devcontainer/stripe-to-invoice/docker-compose.yml +++ b/.devcontainer/stripe-to-invoice/docker-compose.yml @@ -2,11 +2,10 @@ version: '3.8' services: one_repo_to_rule_them_all: - # user: "${UID}:${GID}" build: context: ../.. dockerfile: .devcontainer/stripe-to-invoice/Dockerfile - command: code-server --bind-addr 0.0.0.0:8080 --auth password + command: su - vscode -c "code-server --bind-addr 0.0.0.0:8080" # command: sleep infinity volumes: - ../..:/workspaces/monorepo diff --git a/code-server/codeserver.yaml b/code-server/codeserver.yaml index d3646d3..18e1275 100644 --- a/code-server/codeserver.yaml +++ b/code-server/codeserver.yaml @@ -1,29 +1,3 @@ -# ====================================================== -# DEV.JUNTEKIM.COM → LOCALHOST:8080 -# ====================================================== - ---- -apiVersion: v1 -kind: Service -metadata: - name: dev-juntekim-external -spec: - type: ExternalName - externalName: host.docker.internal # change if needed - ports: - - port: 8080 - ---- -apiVersion: v1 -kind: Endpoints -metadata: - name: dev-juntekim-external -subsets: - - addresses: - - ip: 192.168.0.181 - ports: - - port: 8080 - --- apiVersion: v1 kind: Service @@ -33,30 +7,31 @@ spec: ports: - port: 80 targetPort: 8080 - selector: {} # no selector — used with external endpoints +--- + +apiVersion: v1 +kind: Endpoints +metadata: + name: dev-juntekim-service +subsets: + - addresses: + - ip: 192.168.0.181 # mist node + ports: + - port: 8080 --- -apiVersion: networking.k8s.io/v1 -kind: Ingress +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute metadata: name: dev-juntekim-ingress - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure - cert-manager.io/cluster-issuer: myresolver spec: - ingressClassName: traefik + entryPoints: + - websecure + routes: + - match: Host(`dev.juntekim.com`) + kind: Rule + services: + - name: dev-juntekim-service + port: 80 tls: - - hosts: - - dev.juntekim.com - secretName: dev-juntekim-tls - rules: - - host: dev.juntekim.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: dev-juntekim-service - port: - number: 80 \ No newline at end of file + certResolver: myresolver \ No newline at end of file diff --git a/code-server/sal-codeservery.yaml b/code-server/sal-codeservery.yaml new file mode 100644 index 0000000..010a64b --- /dev/null +++ b/code-server/sal-codeservery.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Service +metadata: + name: sal-juntekim-service +spec: + ports: + - port: 80 + targetPort: 8081 +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: sal-juntekim-service +subsets: + - addresses: + - ip: 192.168.0.181 # mist node + ports: + - port: 8081 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: sal-juntekim-ingress +spec: + entryPoints: + - websecure + routes: + - match: Host(`sal.juntekim.com`) + kind: Rule + services: + - name: sal-juntekim-service + port: 80 + tls: + certResolver: myresolver \ No newline at end of file diff --git a/stripe_to_invoice/README.md b/stripe_to_invoice/README.md deleted file mode 100644 index cb41c53..0000000 --- a/stripe_to_invoice/README.md +++ /dev/null @@ -1,248 +0,0 @@ -Got you — here’s a clean, founder-brain-friendly summary of **Stripe → Invoice (Stripe → Xero)** based on everything you’ve been working through, plus **tight next steps** that fit your nights/weekends reality. - ---- - -## 🧾 What Stripe → Invoice Is (current state) - -* **Problem you’re solving** - - * UK VAT-registered small businesses using Stripe struggle with **audit-safe, VAT-correct invoices** in Xero - * Existing tools are overbuilt, accountant-first, or break down on VAT, clearing accounts, or reconciliation - * This is fundamentally a **VAT + audit correctness problem**, not just “sync data” - -* **Who it’s for** - - * UK solo founders / one-person companies / tiny teams - * Using **Stripe only** (Payment Links + Subscriptions) - * Using **Xero** - * Not accountants, not agencies, not complex multi-channel setups - -* **What the MVP does today** - - * Stripe OAuth + Xero OAuth both working - * Webhooks flow end-to-end (validated against real finance manager) - * Automatically: - - * Creates **clean Xero invoices** from Stripe payments - * Applies VAT correctly - * Posts payments via a **Stripe Clearing account** - * Validated by a finance manager → *very happy* (huge signal) - -* **Key MVP constraints (intentional)** - - * UK + GBP only - * Stripe Payment Links + Subscriptions only - * Xero contacts matched/created by **email only** - * Willing to: - - * Run one-off scripts - * Do manual fixes early - * Goal: **first ~5 paying customers**, not scale yet - ---- - -## ✅ Recently fixed - -* **Xero contact creation** — Now checks for existing contacts by email first, reuses if found, only creates if missing -* **Stripe OAuth app reuse** — Added unique constraints on `userId` and `stripeAccountId` to prevent duplicate connections -* **Smart redirect flow** — Users are automatically routed based on connection state: - * Both connected → `/dashboard` - * Only Stripe → `/connect/xero` - * No connections → `/connect/stripe` -* **Connection visibility** — Dashboard now displays connected Stripe account ID and Xero tenant ID - -### Frontend Improvements Details - -**Smart Onboarding Flow** -* Automatic routing based on connection state -* Users never see unnecessary steps -* Seamless progression: Login → Stripe → Xero → Dashboard - -**Dashboard Enhancements** -* Connected account visibility (Stripe account ID + Xero tenant) -* Account code configuration (sales + clearing accounts) -* Real-time save confirmation -* Clean, minimal UI - -**Development Experience** -* Development mode fallback for webhook testing -* Comprehensive logging at each webhook stage -* Environment-aware configuration - ---- - -## ⚠️ Known issues & TODO - -* **CRITICAL: Stripe payment integration** - * Need to implement Stripe Billing API to accept payments - * Currently not accepting any money from users (test mode only) - * Must add subscription checkout flow before going live - * Reference: [Stripe Billing API docs](https://stripe.com/docs/billing) - -* **Missing UX guardrails:** - * No clear **pre-payment checklist** before enabling sync - ---- - -## 🧪 Current mode you’re in (important) - -* You’re correctly running this in **“design partner / friend test” mode** - - * Payments disabled - * Banner: *“Internal test – not a commercial product”* - * Clear paper trail of non-commercial intent -* CFO + finance manager already acting as **design partners** -* This massively de-risks VAT/audit assumptions before charging anyone - ---- - -## ✅ What you should do next (ordered, ruthless, realistic) - -### 1️⃣ Finish the last **correctness blockers** (highest ROI) - -These unlock charging real money. - -* [x] ~~Fix Xero contact creation~~ ✅ DONE - - * ~~Check by email → reuse if exists → only create if missing~~ -* [x] ~~Fix Stripe OAuth app reuse (stop creating new apps)~~ ✅ DONE -* [x] ~~Re-enable "mark invoice as paid" via Stripe Clearing once accounts are valid~~ ✅ DONE - -> Outcome: rock-solid, boring, accountant-approved flow - ---- - -### 2️⃣ Add a tiny **pre-flight checklist UI** (not a full settings page) - -* [x] ~~Dashboard shows connected accounts~~ ✅ DONE - - * ~~Stripe account ID displayed~~ - * ~~Xero tenant ID displayed~~ -* [x] ~~Smart redirect flow based on connection state~~ ✅ DONE -* [ ] VAT status detection -* [ ] Sales account code shown (editable) -* [ ] Stripe clearing account shown (editable) - -> Even basic connection visibility prevents 80% of future support pain - ---- - -### 3️⃣ Implement subscription billing (enables first paid customer) - -* Integrate Stripe Billing for subscription management -* Add usage tracking (invoice count per month) -* Create pricing page and checkout flow -* Implement subscription status checks in webhook handler -* Remove "internal test" banner once billing is live - ---- - -### 4️⃣ Switch from "design partner" → **first paid customer mode** - -* Pick **one**: - - * A founder you already know **OR** - * A cold UK Stripe + Xero business with obvious VAT needs -* Offer: - - * £15/month Starter plan - * "Early access / founder pricing" (50% off for life) - * Manual support included -* Goal is **money changing hands**, not scale - -> You've said it yourself: getting paid energises you — lean into that. - ---- - -### 5️⃣ Do *targeted* cold outreach (low volume, high signal) - -* 5–10 emails max, not a campaign -* Target: - - * UK SaaS / indie founders - * Stripe Payment Links or Subscriptions - * Clearly VAT-registered -* Lead with: - - * "I built this because my accountant hated existing tools" - * Emphasise **audit-safe, VAT-correct invoices** - * Not "automation", not "syncing" - ---- - -### 6️⃣ Future UX polish + automation (after first paying customers) - -* Auto-detect or create Stripe Clearing account in Xero -* Bulk historical invoice sync -* Invoice preview before creation -* Reduce manual fixes you find yourself repeating -* Nothing else until: - - * You have **~3–5 paying users** - * And they're still using it after month 1 - ---- - -## 💳 SaaS Subscription Model (proposed) - -### Pricing Tiers - -**All Plans — £50/month** -* Unlimited invoices/month -* Stripe Payment Links + Subscriptions support -* Full VAT handling and audit compliance -* Email support -* Perfect for: UK businesses using Stripe + Xero, any size - -**Future tiers** (if needed): -* Starter — £30/month (up to 50 invoices) -* Professional — £50/month (up to 200 invoices) -* Business — £100/month (unlimited) - -### Implementation Notes - -* **Billing via Stripe Checkout** (dogfooding our own product) -* **Monthly recurring subscriptions** with automatic renewal -* **14-day free trial** — no credit card required -* **Founder pricing lock-in** — First 50 customers get lifetime 50% off -* **Usage tracking** — Invoice count displayed in dashboard, soft warnings at 80% of limit -* **Graceful degradation** — Over-limit users get notified but sync continues (no hard cutoff) - -### Revenue Model - -* **Target: 100 paying customers in 6 months** - * 60% Starter (£900/mo) - * 30% Professional (£1,050/mo) - * 10% Business (£750/mo) - * Total: ~£2,700/mo MRR - -* **Conservative burn** - * Hosting: £50/mo (Vercel + DB) - * Email: £10/mo (AWS SES) - * Support: Founder time only - * Net: ~£2,640/mo profit margin - -### Next Steps for Monetization - -1. Add Stripe Billing integration to the app -2. Implement usage tracking in webhook handler -3. Create pricing page on landing site -4. Add subscription management in dashboard -5. Enable payments and remove "internal test" banner - ---- - -## 🧠 The big picture (sanity check) - -* You’re *not* early anymore — you’re **post-validation, pre-pricing** -* The hard bit (VAT correctness + finance approval) is already done -* The remaining work is boring plumbing + selling -* This is exactly where most side projects die — don’t overbuild now - -If you want, next we can: - -* Draft the **first cold email** -* Write the **“Why this exists” landing page copy** -* Or map a **2-week nights/weekends execution plan** - -Just say the word.