--- # JARR Dev Environment # Collaborative digital jar web application # # Prerequisites: # 1. Replace 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:8-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.38 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.38 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.38 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.38 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.38 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