Files
homelab-manifests/jarrs-system/jarr-dev.yaml
T
2026-06-02 20:03:02 +02:00

705 lines
21 KiB
YAML

---
# JARR Dev Environment
# Collaborative digital jar web application
#
# Prerequisites:
# 1. Replace <CHANGE-ME> placeholders in secrets below
# 2. Build and push the image: gitea.dooplex.hu/admin/jarr:dev
#
# Generate JWT secret:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
---
apiVersion: v1
kind: Namespace
metadata:
name: jarrs-system
labels:
app.kubernetes.io/name: jarr
---
# PostgreSQL PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dev-jarr-postgres
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
# Redis PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dev-jarr-redis
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
---
# PostgreSQL 16
apiVersion: apps/v1
kind: Deployment
metadata:
name: dev-jarr-postgres
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
template:
metadata:
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
spec:
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: password
- name: POSTGRES_DB
value: jarr_dev
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
name: postgres
protocol: TCP
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
subPath: data
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
livenessProbe:
exec:
command:
- pg_isready
- -U
- jarr_dev
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 5
timeoutSeconds: 5
readinessProbe:
exec:
command:
- pg_isready
- -U
- jarr_dev
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: dev-jarr-postgres
---
apiVersion: v1
kind: Service
metadata:
name: dev-jarr-postgres
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: postgres
protocol: TCP
name: postgres
selector:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: postgres
---
# Redis 7
apiVersion: apps/v1
kind: Deployment
metadata:
name: dev-jarr-redis
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
template:
metadata:
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
spec:
containers:
- name: redis
image: redis:7-alpine
imagePullPolicy: IfNotPresent
args:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "64mb"
- --maxmemory-policy
- "noeviction"
ports:
- containerPort: 6379
name: redis
protocol: TCP
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: dev-jarr-redis
---
apiVersion: v1
kind: Service
metadata:
name: dev-jarr-redis
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
spec:
type: ClusterIP
ports:
- port: 6379
targetPort: redis
protocol: TCP
name: redis
selector:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: redis
---
# JARR Application
apiVersion: apps/v1
kind: Deployment
metadata:
name: dev-jarr
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
template:
metadata:
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z dev-jarr-postgres 5432; do
echo "PostgreSQL not ready, waiting..."
sleep 2
done
echo "PostgreSQL is ready!"
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for Redis..."
until nc -z dev-jarr-redis 6379; do
echo "Redis not ready, waiting..."
sleep 2
done
echo "Redis is ready!"
- name: run-migrations
image: gitea.dooplex.hu/admin/jarr:latest
imagePullPolicy: Always
command: ["node", "apps/api/dist/migrate.js"]
env:
- name: NODE_ENV
value: development
- name: DB_USER
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: password
- name: DATABASE_URL
value: "postgresql://$(DB_USER):$(DB_PASS)@dev-jarr-postgres:5432/jarr_dev"
- name: REDIS_URL
value: "redis://dev-jarr-redis:6379"
- name: JWT_ACCESS_SECRET
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: jwt-access-secret
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
containers:
- name: jarr
image: gitea.dooplex.hu/admin/jarr:latest
imagePullPolicy: Always
env:
- name: NODE_ENV
value: development
- name: PORT
value: "3000"
- name: BASE_URL
value: "https://dev.jarrs.eu"
- name: WEB_URL
value: "https://dev.jarrs.eu"
# Database
- name: DB_USER
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: password
- name: DATABASE_URL
value: "postgresql://$(DB_USER):$(DB_PASS)@dev-jarr-postgres:5432/jarr_dev"
# Redis
- name: REDIS_URL
value: "redis://dev-jarr-redis:6379"
# JWT
- name: JWT_ACCESS_SECRET
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: jwt-access-secret
- name: JWT_ACCESS_EXPIRES_IN
value: "15m"
- name: JWT_REFRESH_EXPIRES_IN
value: "30d"
# Email (Resend)
- name: RESEND_API_KEY
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: resend-api-key
- name: EMAIL_FROM
value: "noreply@jarrs.eu"
# Rate limiting
- name: RATE_LIMIT_ENABLED
value: "false"
- name: GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: GOOGLE_CLIENT_ID
- name: GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: GOOGLE_CLIENT_SECRET
- name: DISCORD_CLIENT_ID
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: DISCORD_CLIENT_ID
- name: DISCORD_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: DISCORD_CLIENT_SECRET
ports:
- containerPort: 3000
name: http
protocol: TCP
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
startupProbe:
httpGet:
path: /v1/health
port: http
periodSeconds: 10
failureThreshold: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /v1/health
port: http
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /v1/health
port: http
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 5
timeoutSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: dev-jarr
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
spec:
type: ClusterIP
ports:
- port: 3000
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
---
# =============================================================================
# PATCH: jarr-dev.yaml — Ingress section replacement
# =============================================================================
# Replace the existing `# Ingress` block (from line 473 to end of file) with
# the block below.
#
# What changed:
# Added nginx.ingress.kubernetes.io/configuration-snippet with security
# headers. These apply to ALL responses (SPA root + API routes) at the
# nginx layer, which is the correct place since the SPA at / is outside
# Hono's /v1 basePath middleware chain.
#
# PREREQUISITE — check snippet annotations are allowed in your cluster:
# kubectl -n ingress-nginx get configmap ingress-nginx-controller -o yaml | grep allow-snippet
# If not present or set to "false", add it:
# kubectl -n ingress-nginx edit configmap ingress-nginx-controller
# → add under data: allow-snippet-annotations: "true"
#
# NOTE: HSTS (strict-transport-security) is intentionally NOT in the snippet —
# it is already applied by nginx-ingress automatically when TLS is configured.
# Adding it here would produce a duplicate header.
#
# NOTE on CSP: This CSP is tuned for a Vite/React SPA.
# After applying, open https://dev.jarrs.eu in browser DevTools → Console
# and check for any CSP violations. If you see violations, report them
# and update the policy before pushing to production.
#
# APPLY:
# kubectl apply -f jarr-dev.yaml
# kubectl -n jarrs-system rollout status deployment/dev-jarr
# =============================================================================
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-jarr
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/hostname: dev.jarrs.eu
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://stats.felhom.eu; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https://stats.felhom.eu; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
spec:
ingressClassName: nginx-internal
rules:
- host: dev.jarrs.eu
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dev-jarr
port:
number: 3000
tls:
- hosts:
- dev.jarrs.eu
secretName: dev-jarr-tls
---
# =============================================================================
# JARR Worker Deployment
# =============================================================================
# Separate deployment for the BullMQ background worker.
# Uses the same Docker image as the API, with CMD override.
#
# Processes: token cleanup (hourly), scheduled pulls, pre-flight checks.
# Publishes real-time events via EventBus (Redis Pub/Sub).
#
# No Service or Ingress needed — worker doesn't accept inbound traffic.
# k8s probes access the container health endpoint directly via pod IP.
#
# APPLY (append to jarr-dev.yaml or apply separately):
# kubectl apply -f dev-jarr-worker.yaml
# kubectl -n jarrs-system rollout status deployment/dev-jarr-worker
#
# VERIFY:
# kubectl -n jarrs-system get pods -l app.kubernetes.io/component=worker
# kubectl -n jarrs-system logs deploy/dev-jarr-worker -c jarr-worker --tail=50
# kubectl exec -n jarrs-system deploy/dev-jarr-worker -c jarr-worker -- wget -qO- http://localhost:3001/health
# =============================================================================
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dev-jarr-worker
namespace: jarrs-system
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: worker
spec:
replicas: 1
strategy:
type: Recreate # No rolling update needed — single worker is fine
selector:
matchLabels:
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: worker
template:
metadata:
labels:
app.kubernetes.io/name: jarr
app.kubernetes.io/instance: dev-jarr
app.kubernetes.io/component: worker
spec:
initContainers:
# 1. Wait for PostgreSQL to accept connections
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z dev-jarr-postgres 5432; do
echo "PostgreSQL not ready, waiting..."
sleep 2
done
echo "PostgreSQL is ready!"
# 2. Wait for Redis to accept connections
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for Redis..."
until nc -z dev-jarr-redis 6379; do
echo "Redis not ready, waiting..."
sleep 2
done
echo "Redis is ready!"
# 3. Wait for API to be healthy (migrations complete)
# Prevents the worker from picking up stale queued jobs
# before schema migrations have been applied.
- name: wait-for-api
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for API to be healthy (migrations done)..."
until wget -qO- http://dev-jarr:3000/v1/health >/dev/null 2>&1; do
echo "API not ready, waiting..."
sleep 3
done
echo "API is healthy!"
containers:
- name: jarr-worker
image: gitea.dooplex.hu/admin/jarr:latest
imagePullPolicy: Always
command: ["node", "apps/api/dist/worker.js"]
env:
- name: NODE_ENV
value: development
# Database
- name: DB_USER
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: dev-jarr-db
key: password
- name: DATABASE_URL
value: "postgresql://$(DB_USER):$(DB_PASS)@dev-jarr-postgres:5432/jarr_dev"
# Redis
- name: REDIS_URL
value: "redis://dev-jarr-redis:6379"
# JWT (needed by NotificationService for HMAC unsubscribe tokens)
- name: JWT_ACCESS_SECRET
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: jwt-access-secret
# Email (scheduled pulls trigger notifications → emails)
- name: RESEND_API_KEY
valueFrom:
secretKeyRef:
name: dev-jarr-app
key: resend-api-key
- name: EMAIL_FROM
value: "noreply@jarrs.eu"
# URLs (used in email templates for links)
- name: BASE_URL
value: "https://dev.jarrs.eu"
- name: WEB_URL
value: "https://dev.jarrs.eu"
# Worker health port (matches default, explicit for clarity)
- name: WORKER_HEALTH_PORT
value: "3001"
ports:
- containerPort: 3001
name: health
protocol: TCP
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
startupProbe:
httpGet:
path: /health
port: health
periodSeconds: 5
failureThreshold: 12 # 60s max startup (Redis connect + scheduler registration)
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /health
port: health
initialDelaySeconds: 5
periodSeconds: 15
failureThreshold: 3
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health
port: health
initialDelaySeconds: 15
periodSeconds: 30
failureThreshold: 5
timeoutSeconds: 5