--- # 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:7-alpine imagePullPolicy: IfNotPresent args: - redis-server - --appendonly - "yes" - --maxmemory - "64mb" - --maxmemory-policy - "allkeys-lru" 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 - 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 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" 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