Skip to content

Kubernetes deployment

Manifests for a single-replica dashboard. HA patterns build on this — read the constraints there before scaling.

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: festin
  labels: {app: festin}
spec:
  replicas: 1                    # see HA page before raising this
  strategy: {type: Recreate}     # SQLite volume can't be shared by two pods
  selector:
    matchLabels: {app: festin}
  template:
    metadata:
      labels: {app: festin}
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 999
        fsGroup: 999
      containers:
        - name: festin
          image: ghcr.io/cr0hn/festin:latest
          args: ["serve", "--host", "0.0.0.0", "--db", "/data/festin.db"]
          env:
            - name: FESTIN_JWT_SECRET
              valueFrom:
                secretKeyRef: {name: festin-secrets, key: jwt-secret}
          ports:
            - containerPort: 8420
              name: http
          volumeMounts:
            - name: data
              mountPath: /data
          resources:
            requests: {cpu: 100m, memory: 128Mi}
            limits: {cpu: "1", memory: 512Mi}
          readinessProbe:
            httpGet: {path: /api/v1/health, port: 8420}
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: {path: /api/v1/health, port: 8420}
            initialDelaySeconds: 15
            periodSeconds: 20
            failureThreshold: 3
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: festin-data

Secret

kubectl create secret generic festin-secrets \
  --from-literal=jwt-secret="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"

PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: festin-data
spec:
  accessModes: ["ReadWriteOnce"]   # single-node attach — matches SQLite
  resources:
    requests:
      storage: 5Gi

Service + Ingress

apiVersion: v1
kind: Service
metadata:
  name: festin
spec:
  selector: {app: festin}
  ports:
    - port: 80
      targetPort: 8420
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: festin
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "5"          # brute-force guard for /auth/login
spec:
  rules:
    - host: festin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: festin
                port: {number: 80}

Scanner as a CronJob

One-shot scans fit Kubernetes CronJobs perfectly:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: festin-recon
spec:
  schedule: "0 6 * * 1"          # Mondays 06:00
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: scan
              image: ghcr.io/cr0hn/festin:latest
              args:
                - scan
                - -f
                - /etc/festin/targets.txt
                - --export
                - sarif
                - --output
                - /output/weekly.sarif
                - --quiet
              volumeMounts:
                - name: targets
                  mountPath: /etc/festin
                - name: output
                  mountPath: /output
          volumes:
            - name: targets
              configMap: {name: festin-targets}
            - name: output
              persistentVolumeClaim: {claimName: festin-reports}

Why replicas: 1 and strategy: Recreate (SQLite topology)

The manifest above keeps SQLite on a ReadWriteOnce volume: two pods can't share it and a rolling update would overlap pods on the same claim.

For multi-replica production, switch to PostgreSQL + streaQ — set FESTIN_DB_DSN, FESTIN_QUEUE=streaq, run festin-worker Deployments, then raise replicas and use RollingUpdate freely. Full walkthrough: high availability.