diff --git a/deploy/k8s/helm/Chart.yaml b/deploy/k8s/helm/Chart.yaml new file mode 100644 index 00000000..c0e135d5 --- /dev/null +++ b/deploy/k8s/helm/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: finmind +description: FinMind — AI-powered personal finance manager (Flask + React + PostgreSQL + Redis) +type: application +version: 1.0.0 +appVersion: "1.0.0" + +keywords: + - finmind + - finance + - flask + - react + - postgresql + +home: https://github.com/rohitdash08/FinMind +sources: + - https://github.com/rohitdash08/FinMind + +maintainers: + - name: rohitdash08 + url: https://github.com/rohitdash08 + +dependencies: [] diff --git a/deploy/k8s/helm/templates/configmap.yaml b/deploy/k8s/helm/templates/configmap.yaml new file mode 100644 index 00000000..63a70b1d --- /dev/null +++ b/deploy/k8s/helm/templates/configmap.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: finmind-config + namespace: {{ .Values.global.namespace }} + labels: + app.kubernetes.io/name: finmind + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +data: + LOG_LEVEL: {{ .Values.backend.env.LOG_LEVEL | quote }} + GEMINI_MODEL: {{ .Values.backend.env.GEMINI_MODEL | quote }} + REDIS_URL: "redis://redis:{{ .Values.redis.port }}/0" + DATABASE_URL: "postgresql://finmind:$(POSTGRES_PASSWORD)@postgres:{{ .Values.postgresql.port }}/{{ .Values.postgresql.database }}" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-config + namespace: {{ .Values.global.namespace }} + labels: + app.kubernetes.io/name: finmind-nginx + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +data: + default.conf: | + upstream backend { + server backend:{{ .Values.backend.port }}; + } + server { + listen {{ .Values.nginx.port }}; + server_name _; + + location /api/ { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_connect_timeout 10s; + } + + location /health { + proxy_pass http://backend/health; + } + + location /metrics { + proxy_pass http://backend/metrics; + } + + location / { + proxy_pass http://frontend:{{ .Values.frontend.port }}; + proxy_set_header Host $host; + } + } diff --git a/deploy/k8s/helm/templates/deployment.yaml b/deploy/k8s/helm/templates/deployment.yaml new file mode 100644 index 00000000..5e722d29 --- /dev/null +++ b/deploy/k8s/helm/templates/deployment.yaml @@ -0,0 +1,341 @@ +# ============================================================================= +# PostgreSQL +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: {{ .Values.global.namespace }} + labels: + app: postgres + app.kubernetes.io/name: finmind-postgres + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + replicas: {{ .Values.postgresql.replicaCount }} + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - containerPort: {{ .Values.postgresql.port }} + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_DB + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + readinessProbe: + exec: + command: ["pg_isready", "-U", "finmind"] + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["pg_isready", "-U", "finmind"] + initialDelaySeconds: 30 + periodSeconds: 30 + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + {{- if .Values.postgresql.persistence.enabled }} + persistentVolumeClaim: + claimName: postgres-data + {{- else }} + emptyDir: {} + {{- end }} +--- +# ============================================================================= +# Redis +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: {{ .Values.global.namespace }} + labels: + app: redis + app.kubernetes.io/name: finmind-redis + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + replicas: {{ .Values.redis.replicaCount }} + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - containerPort: {{ .Values.redis.port }} + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 15 + periodSeconds: 20 +--- +# ============================================================================= +# Backend (Flask/Gunicorn) +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: {{ .Values.global.namespace }} + labels: + app: backend + app.kubernetes.io/name: finmind-backend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.backend.port }}" + prometheus.io/path: "/metrics" +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.backend.port }}" + prometheus.io/path: "/metrics" + spec: + initContainers: + - name: db-migrate + image: "{{ .Values.global.imageRegistry }}/finmind-backend:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + command: ["flask", "init-db"] + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_DB + - name: DATABASE_URL + value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@postgres:{{ .Values.postgresql.port }}/$(POSTGRES_DB)" + containers: + - name: backend + image: "{{ .Values.global.imageRegistry }}/finmind-backend:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - containerPort: {{ .Values.backend.port }} + command: + - sh + - -c + - | + mkdir -p /tmp/prometheus_multiproc + export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc + exec gunicorn -w {{ .Values.backend.gunicornWorkers }} -k gthread \ + --threads {{ .Values.backend.gunicornThreads }} \ + -b 0.0.0.0:{{ .Values.backend.port }} wsgi:app + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: finmind-secrets + key: POSTGRES_DB + - name: DATABASE_URL + value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@postgres:{{ .Values.postgresql.port }}/$(POSTGRES_DB)" + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: finmind-secrets + key: JWT_SECRET + - name: GEMINI_API_KEY + valueFrom: + secretKeyRef: + name: finmind-secrets + key: GEMINI_API_KEY + - name: LOG_LEVEL + valueFrom: + configMapKeyRef: + name: finmind-config + key: LOG_LEVEL + - name: GEMINI_MODEL + valueFrom: + configMapKeyRef: + name: finmind-config + key: GEMINI_MODEL + - name: REDIS_URL + valueFrom: + configMapKeyRef: + name: finmind-config + key: REDIS_URL + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + readinessProbe: + httpGet: + path: {{ .Values.backend.probes.readiness.path }} + port: {{ .Values.backend.port }} + initialDelaySeconds: {{ .Values.backend.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.backend.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.backend.probes.readiness.failureThreshold }} + livenessProbe: + httpGet: + path: {{ .Values.backend.probes.liveness.path }} + port: {{ .Values.backend.port }} + initialDelaySeconds: {{ .Values.backend.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.backend.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.backend.probes.liveness.failureThreshold }} + {{- with .Values.backend.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +--- +# ============================================================================= +# Frontend (React served via Nginx) +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: {{ .Values.global.namespace }} + labels: + app: frontend + app.kubernetes.io/name: finmind-frontend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: "{{ .Values.global.imageRegistry }}/finmind-frontend:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + ports: + - containerPort: {{ .Values.frontend.port }} + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.frontend.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: {{ .Values.frontend.port }} + initialDelaySeconds: 15 + periodSeconds: 20 +--- +# ============================================================================= +# Nginx reverse proxy +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: {{ .Values.global.namespace }} + labels: + app: nginx + app.kubernetes.io/name: finmind-nginx + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + replicas: {{ .Values.nginx.replicaCount }} + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}" + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - containerPort: {{ .Values.nginx.port }} + resources: + {{- toYaml .Values.nginx.resources | nindent 12 }} + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/conf.d + readinessProbe: + httpGet: + path: /health + port: {{ .Values.nginx.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: {{ .Values.nginx.port }} + initialDelaySeconds: 10 + periodSeconds: 20 + volumes: + - name: nginx-config + configMap: + name: nginx-config diff --git a/deploy/k8s/helm/templates/hpa.yaml b/deploy/k8s/helm/templates/hpa.yaml new file mode 100644 index 00000000..00045fa9 --- /dev/null +++ b/deploy/k8s/helm/templates/hpa.yaml @@ -0,0 +1,55 @@ +{{- if .Values.backend.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: backend-hpa + namespace: {{ .Values.global.namespace }} + labels: + app: backend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: backend + minReplicas: {{ .Values.backend.autoscaling.minReplicas }} + maxReplicas: {{ .Values.backend.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.backend.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.backend.autoscaling.targetMemoryUtilizationPercentage }} +{{- end }} +--- +{{- if .Values.frontend.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend-hpa + namespace: {{ .Values.global.namespace }} + labels: + app: frontend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: {{ .Values.frontend.autoscaling.minReplicas }} + maxReplicas: {{ .Values.frontend.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.frontend.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/deploy/k8s/helm/templates/ingress.yaml b/deploy/k8s/helm/templates/ingress.yaml new file mode 100644 index 00000000..384d73bd --- /dev/null +++ b/deploy/k8s/helm/templates/ingress.yaml @@ -0,0 +1,99 @@ +{{- if .Values.ingress.enabled }} +# API Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: finmind-api + namespace: {{ .Values.global.namespace }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + annotations: + {{- range $key, $val := .Values.ingress.annotations }} + {{ $key }}: {{ $val | quote }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.hosts.api }} + secretName: {{ .Values.ingress.tls.secretName }}-api + {{- end }} + rules: + - host: {{ .Values.ingress.hosts.api }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + number: {{ .Values.nginx.port }} +--- +# App (Frontend) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: finmind-app + namespace: {{ .Values.global.namespace }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + annotations: + {{- range $key, $val := .Values.ingress.annotations }} + {{ $key }}: {{ $val | quote }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.hosts.app }} + secretName: {{ .Values.ingress.tls.secretName }}-app + {{- end }} + rules: + - host: {{ .Values.ingress.hosts.app }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: {{ .Values.frontend.port }} +{{- if .Values.monitoring.enabled }} +--- +# Grafana Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: finmind-grafana + namespace: {{ .Values.global.namespace }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + annotations: + {{- range $key, $val := .Values.ingress.annotations }} + {{ $key }}: {{ $val | quote }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.hosts.grafana }} + secretName: {{ .Values.ingress.tls.secretName }}-grafana + {{- end }} + rules: + - host: {{ .Values.ingress.hosts.grafana }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: grafana + port: + number: {{ .Values.monitoring.grafana.port }} +{{- end }} +{{- end }} diff --git a/deploy/k8s/helm/templates/pvc.yaml b/deploy/k8s/helm/templates/pvc.yaml new file mode 100644 index 00000000..3448b728 --- /dev/null +++ b/deploy/k8s/helm/templates/pvc.yaml @@ -0,0 +1,81 @@ +{{- if .Values.postgresql.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data + namespace: {{ .Values.global.namespace }} + labels: + app: postgres + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.postgresql.persistence.size }} + {{- if .Values.postgresql.persistence.storageClass }} + storageClassName: {{ .Values.postgresql.persistence.storageClass }} + {{- else if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} +{{- end }} +{{- if and .Values.monitoring.enabled .Values.monitoring.prometheus.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prometheus-data + namespace: {{ .Values.global.namespace }} + labels: + app: prometheus + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.monitoring.prometheus.persistence.size }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} +{{- end }} +{{- if and .Values.monitoring.enabled .Values.monitoring.loki.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: loki-data + namespace: {{ .Values.global.namespace }} + labels: + app: loki + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.monitoring.loki.persistence.size }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} +{{- end }} +{{- if and .Values.monitoring.enabled .Values.monitoring.grafana.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: grafana-data + namespace: {{ .Values.global.namespace }} + labels: + app: grafana + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.monitoring.grafana.persistence.size }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} +{{- end }} diff --git a/deploy/k8s/helm/templates/secrets.yaml b/deploy/k8s/helm/templates/secrets.yaml new file mode 100644 index 00000000..4562dc4e --- /dev/null +++ b/deploy/k8s/helm/templates/secrets.yaml @@ -0,0 +1,20 @@ +{{- if .Values.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: finmind-secrets + namespace: {{ .Values.global.namespace }} + labels: + app.kubernetes.io/name: finmind + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +type: Opaque +data: + POSTGRES_USER: {{ "finmind" | b64enc | quote }} + POSTGRES_PASSWORD: {{ .Values.secrets.postgresPassword | quote }} + POSTGRES_DB: {{ .Values.postgresql.database | b64enc | quote }} + JWT_SECRET: {{ .Values.secrets.jwtSecret | quote }} + GEMINI_API_KEY: {{ .Values.secrets.geminiApiKey | b64enc | quote }} + GRAFANA_ADMIN_USER: {{ .Values.secrets.grafanaAdminUser | quote }} + GRAFANA_ADMIN_PASSWORD: {{ .Values.secrets.grafanaAdminPassword | quote }} +{{- end }} diff --git a/deploy/k8s/helm/templates/service.yaml b/deploy/k8s/helm/templates/service.yaml new file mode 100644 index 00000000..6e7d2a37 --- /dev/null +++ b/deploy/k8s/helm/templates/service.yaml @@ -0,0 +1,142 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: {{ .Values.global.namespace }} + labels: + app: postgres + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: postgres + ports: + - port: {{ .Values.postgresql.port }} + targetPort: {{ .Values.postgresql.port }} + protocol: TCP + name: postgres +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: {{ .Values.global.namespace }} + labels: + app: redis + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: redis + ports: + - port: {{ .Values.redis.port }} + targetPort: {{ .Values.redis.port }} + protocol: TCP + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: {{ .Values.global.namespace }} + labels: + app: backend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: backend + ports: + - port: {{ .Values.backend.port }} + targetPort: {{ .Values.backend.port }} + protocol: TCP + name: http +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: {{ .Values.global.namespace }} + labels: + app: frontend + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: frontend + ports: + - port: {{ .Values.frontend.port }} + targetPort: {{ .Values.frontend.port }} + protocol: TCP + name: http +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: {{ .Values.global.namespace }} + labels: + app: nginx + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: nginx + ports: + - port: {{ .Values.nginx.port }} + targetPort: {{ .Values.nginx.port }} + protocol: TCP + name: http +{{- if .Values.monitoring.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: {{ .Values.global.namespace }} + labels: + app: prometheus + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: prometheus + ports: + - port: {{ .Values.monitoring.prometheus.port }} + targetPort: {{ .Values.monitoring.prometheus.port }} + name: http +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + namespace: {{ .Values.global.namespace }} + labels: + app: grafana + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: grafana + ports: + - port: {{ .Values.monitoring.grafana.port }} + targetPort: {{ .Values.monitoring.grafana.port }} + name: http +--- +apiVersion: v1 +kind: Service +metadata: + name: loki + namespace: {{ .Values.global.namespace }} + labels: + app: loki + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +spec: + type: ClusterIP + selector: + app: loki + ports: + - port: {{ .Values.monitoring.loki.port }} + targetPort: {{ .Values.monitoring.loki.port }} + name: http +{{- end }} diff --git a/deploy/k8s/helm/values.yaml b/deploy/k8s/helm/values.yaml new file mode 100644 index 00000000..4702dda4 --- /dev/null +++ b/deploy/k8s/helm/values.yaml @@ -0,0 +1,201 @@ +# ============================================================================= +# FinMind Helm Chart — Default Values +# Override with: helm install finmind ./deploy/k8s/helm -f my-values.yaml +# ============================================================================= + +global: + namespace: finmind + imageRegistry: ghcr.io/rohitdash08 + imagePullPolicy: IfNotPresent + storageClass: "" # "" uses cluster default + +# --------------------------------------------------------------------------- +# Backend (Flask/Gunicorn) +# --------------------------------------------------------------------------- +backend: + image: + repository: ghcr.io/rohitdash08/finmind-backend + tag: latest + pullPolicy: IfNotPresent + replicaCount: 2 + port: 8000 + gunicornWorkers: 2 + gunicornThreads: 4 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + probes: + readiness: + path: /health + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + liveness: + path: /health + initialDelaySeconds: 20 + periodSeconds: 20 + failureThreshold: 3 + env: + LOG_LEVEL: INFO + GEMINI_MODEL: gemini-1.5-flash + nodeSelector: {} + tolerations: [] + affinity: {} + +# --------------------------------------------------------------------------- +# Frontend (Nginx serving React build) +# --------------------------------------------------------------------------- +frontend: + image: + repository: ghcr.io/rohitdash08/finmind-frontend + tag: latest + pullPolicy: IfNotPresent + replicaCount: 2 + port: 80 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 6 + targetCPUUtilizationPercentage: 70 + nodeSelector: {} + tolerations: [] + affinity: {} + +# --------------------------------------------------------------------------- +# PostgreSQL +# --------------------------------------------------------------------------- +postgresql: + image: + repository: postgres + tag: "16" + replicaCount: 1 + port: 5432 + database: finmind + username: finmind + # password is sourced from secret: finmind-secrets + persistence: + enabled: true + size: 10Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +# --------------------------------------------------------------------------- +# Redis +# --------------------------------------------------------------------------- +redis: + image: + repository: redis + tag: "7" + replicaCount: 1 + port: 6379 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +# --------------------------------------------------------------------------- +# Nginx reverse proxy +# --------------------------------------------------------------------------- +nginx: + image: + repository: nginx + tag: 1.27-alpine + replicaCount: 1 + port: 8080 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +# --------------------------------------------------------------------------- +# Ingress +# --------------------------------------------------------------------------- +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + api: api.finmind.example.com + app: app.finmind.example.com + grafana: grafana.finmind.example.com + tls: + enabled: true + secretName: finmind-tls + +# --------------------------------------------------------------------------- +# Monitoring +# --------------------------------------------------------------------------- +monitoring: + enabled: true + prometheus: + image: + repository: prom/prometheus + tag: v2.54.1 + port: 9090 + retention: 14d + retentionSize: 1GB + persistence: + enabled: true + size: 5Gi + loki: + image: + repository: grafana/loki + tag: 2.9.10 + port: 3100 + retention: 168h + persistence: + enabled: true + size: 5Gi + grafana: + image: + repository: grafana/grafana + tag: 11.1.5 + port: 3000 + persistence: + enabled: true + size: 2Gi + # admin credentials sourced from secret: finmind-secrets + +# --------------------------------------------------------------------------- +# Secrets (values here are base64-encoded defaults for dev — CHANGE IN PROD) +# --------------------------------------------------------------------------- +secrets: + # Set to false to manage secrets externally (e.g., via Vault / ESO) + create: true + postgresPassword: CHANGEME-strong-password + jwtSecret: CHANGEME-long-random-secret-min-32-chars + geminiApiKey: "" + grafanaAdminUser: finmind_admin + grafanaAdminPassword: CHANGEME-admin-password diff --git a/deploy/platforms/aws/task-definition.json b/deploy/platforms/aws/task-definition.json new file mode 100644 index 00000000..9d3da816 --- /dev/null +++ b/deploy/platforms/aws/task-definition.json @@ -0,0 +1,89 @@ +{ + "family": "finmind", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole", + "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskRole", + + "containerDefinitions": [ + { + "name": "backend", + "image": "ghcr.io/rohitdash08/finmind-backend:latest", + "essential": true, + "portMappings": [ + { + "containerPort": 8000, + "hostPort": 8000, + "protocol": "tcp" + } + ], + "command": [ + "sh", "-c", + "flask init-db && mkdir -p /tmp/prometheus_multiproc && export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc && exec gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:8000 wsgi:app" + ], + "environment": [ + { "name": "LOG_LEVEL", "value": "INFO" }, + { "name": "GEMINI_MODEL", "value": "gemini-1.5-flash" } + ], + "secrets": [ + { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:finmind/DATABASE_URL" }, + { "name": "REDIS_URL", "valueFrom": "arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:finmind/REDIS_URL" }, + { "name": "JWT_SECRET", "valueFrom": "arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:finmind/JWT_SECRET" }, + { "name": "GEMINI_API_KEY", "valueFrom": "arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:finmind/GEMINI_API_KEY" } + ], + "healthCheck": { + "command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"], + "interval": 15, + "timeout": 5, + "retries": 3, + "startPeriod": 20 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/finmind", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "backend" + } + }, + "resourceRequirements": [] + }, + { + "name": "frontend", + "image": "ghcr.io/rohitdash08/finmind-frontend:latest", + "essential": true, + "portMappings": [ + { + "containerPort": 80, + "hostPort": 80, + "protocol": "tcp" + } + ], + "healthCheck": { + "command": ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"], + "interval": 15, + "timeout": 5, + "retries": 3, + "startPeriod": 10 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/finmind", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "frontend" + } + } + } + ], + + "volumes": [], + "placementConstraints": [], + + "tags": [ + { "key": "Project", "value": "finmind" }, + { "key": "Environment", "value": "production" } + ] +} diff --git a/deploy/platforms/azure/containerapps.yaml b/deploy/platforms/azure/containerapps.yaml new file mode 100644 index 00000000..9e150834 --- /dev/null +++ b/deploy/platforms/azure/containerapps.yaml @@ -0,0 +1,119 @@ +# FinMind — Azure Container Apps deployment +# Deploy: +# az containerapp env create -n finmind-env -g finmind-rg --location eastus +# az containerapp create --yaml deploy/platforms/azure/containerapps.yaml \ +# -g finmind-rg -n finmind-backend +# Docs: https://learn.microsoft.com/en-us/azure/container-apps/azure-resource-manager-api-spec + +# --------------------------------------------------------------------------- +# Backend Container App +# --------------------------------------------------------------------------- +type: Microsoft.App/containerApps +name: finmind-backend +location: eastus +properties: + managedEnvironmentId: /subscriptions/SUBSCRIPTION_ID/resourceGroups/finmind-rg/providers/Microsoft.App/managedEnvironments/finmind-env + configuration: + activeRevisionsMode: Single + ingress: + external: true + targetPort: 8000 + transport: auto + allowInsecure: false + traffic: + - latestRevision: true + weight: 100 + secrets: + - name: database-url + keyVaultUrl: https://finmind-kv.vault.azure.net/secrets/DATABASE-URL + identity: system + - name: redis-url + keyVaultUrl: https://finmind-kv.vault.azure.net/secrets/REDIS-URL + identity: system + - name: jwt-secret + keyVaultUrl: https://finmind-kv.vault.azure.net/secrets/JWT-SECRET + identity: system + - name: gemini-api-key + keyVaultUrl: https://finmind-kv.vault.azure.net/secrets/GEMINI-API-KEY + identity: system + template: + initContainers: + - name: db-migrate + image: ghcr.io/rohitdash08/finmind-backend:latest + command: ["flask", "init-db"] + env: + - name: DATABASE_URL + secretRef: database-url + containers: + - name: backend + image: ghcr.io/rohitdash08/finmind-backend:latest + command: + - sh + - -c + - | + mkdir -p /tmp/prometheus_multiproc + export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc + exec gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:8000 wsgi:app + env: + - name: DATABASE_URL + secretRef: database-url + - name: REDIS_URL + secretRef: redis-url + - name: JWT_SECRET + secretRef: jwt-secret + - name: GEMINI_API_KEY + secretRef: gemini-api-key + - name: LOG_LEVEL + value: INFO + - name: GEMINI_MODEL + value: gemini-1.5-flash + resources: + cpu: 0.5 + memory: 512Mi + probes: + - type: readiness + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + - type: liveness + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 20 + scale: + minReplicas: 1 + maxReplicas: 10 + rules: + - name: http-scale + http: + metadata: + concurrentRequests: "50" +--- +# --------------------------------------------------------------------------- +# Frontend Container App +# --------------------------------------------------------------------------- +type: Microsoft.App/containerApps +name: finmind-frontend +location: eastus +properties: + managedEnvironmentId: /subscriptions/SUBSCRIPTION_ID/resourceGroups/finmind-rg/providers/Microsoft.App/managedEnvironments/finmind-env + configuration: + activeRevisionsMode: Single + ingress: + external: true + targetPort: 80 + transport: auto + allowInsecure: false + template: + containers: + - name: frontend + image: ghcr.io/rohitdash08/finmind-frontend:latest + resources: + cpu: 0.25 + memory: 128Mi + scale: + minReplicas: 1 + maxReplicas: 5 diff --git a/deploy/platforms/digitalocean/app.yaml b/deploy/platforms/digitalocean/app.yaml new file mode 100644 index 00000000..f4291890 --- /dev/null +++ b/deploy/platforms/digitalocean/app.yaml @@ -0,0 +1,96 @@ +# FinMind — DigitalOcean App Platform spec +# Deploy: doctl apps create --spec deploy/platforms/digitalocean/app.yaml +# Docs: https://docs.digitalocean.com/products/app-platform/reference/app-spec/ + +name: finmind +region: nyc + +# --------------------------------------------------------------------------- +# Backend service +# --------------------------------------------------------------------------- +services: + - name: backend + dockerfile_path: packages/backend/Dockerfile + source_dir: packages/backend + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + http_port: 8000 + instance_count: 1 + instance_size_slug: basic-xs + run_command: > + flask init-db && + gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:8080 wsgi:app + health_check: + http_path: /health + initial_delay_seconds: 15 + period_seconds: 10 + timeout_seconds: 5 + failure_threshold: 3 + envs: + - key: DATABASE_URL + scope: RUN_TIME + value: ${db.DATABASE_URL} + - key: REDIS_URL + scope: RUN_TIME + value: ${redis.REDIS_URL} + - key: JWT_SECRET + scope: RUN_AND_BUILD_TIME + type: SECRET + - key: GEMINI_API_KEY + scope: RUN_TIME + type: SECRET + - key: LOG_LEVEL + scope: RUN_TIME + value: INFO + - key: GEMINI_MODEL + scope: RUN_TIME + value: gemini-1.5-flash + alerts: + - rule: CPU_UTILIZATION + operator: GREATER_THAN + value: 80 + window: FIVE_MINUTES + - rule: MEM_UTILIZATION + operator: GREATER_THAN + value: 80 + window: FIVE_MINUTES + + # ------------------------------------------------------------------------- + # Frontend static site + # ------------------------------------------------------------------------- + - name: frontend + dockerfile_path: app/Dockerfile + source_dir: app + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + http_port: 80 + instance_count: 1 + instance_size_slug: basic-xs + health_check: + http_path: / + envs: + - key: VITE_API_URL + scope: BUILD_TIME + value: ${backend.PUBLIC_URL} + +# --------------------------------------------------------------------------- +# Managed databases +# --------------------------------------------------------------------------- +databases: + - name: db + engine: PG + version: "16" + size: db-s-1vcpu-1gb + num_nodes: 1 + production: false + + - name: redis + engine: REDIS + version: "7" + size: db-s-1vcpu-1gb + num_nodes: 1 + eviction_policy: allkeys_lru diff --git a/deploy/platforms/fly/fly.toml b/deploy/platforms/fly/fly.toml new file mode 100644 index 00000000..01f614cf --- /dev/null +++ b/deploy/platforms/fly/fly.toml @@ -0,0 +1,63 @@ +# FinMind — Fly.io deployment configuration +# Deploy: fly launch --config deploy/platforms/fly/fly.toml +# Docs: https://fly.io/docs/reference/configuration/ + +app = "finmind-backend" +primary_region = "iad" +kill_signal = "SIGTERM" +kill_timeout = "30s" + +[build] + dockerfile = "packages/backend/Dockerfile" + build-target = "" + +[env] + PORT = "8000" + LOG_LEVEL = "INFO" + GEMINI_MODEL = "gemini-1.5-flash" + +[deploy] + release_command = "flask init-db" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 1 + processes = ["app"] + + [http_service.concurrency] + type = "requests" + hard_limit = 250 + soft_limit = 200 + +[[http_service.checks]] + grace_period = "10s" + interval = "15s" + method = "GET" + path = "/health" + timeout = "5s" + +[metrics] + port = 8000 + path = "/metrics" + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 512 + +[mounts] + # Uncomment if you want a persistent scratch volume + # source = "finmind_data" + # destination = "/data" + +# --------------------------------------------------------------------------- +# Secrets — set via: fly secrets set KEY=value +# --------------------------------------------------------------------------- +# Required secrets (set before first deploy): +# fly secrets set DATABASE_URL="postgresql://..." +# fly secrets set REDIS_URL="redis://..." +# fly secrets set JWT_SECRET="$(openssl rand -hex 32)" +# fly secrets set GEMINI_API_KEY="your-key" diff --git a/deploy/platforms/gcp/cloudrun-service.yaml b/deploy/platforms/gcp/cloudrun-service.yaml new file mode 100644 index 00000000..7c19a474 --- /dev/null +++ b/deploy/platforms/gcp/cloudrun-service.yaml @@ -0,0 +1,124 @@ +# FinMind — Google Cloud Run service definition +# Deploy: gcloud run services replace deploy/platforms/gcp/cloudrun-service.yaml +# Docs: https://cloud.google.com/run/docs/reference/yaml/v1 + +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: finmind-backend + namespace: PROJECT_ID + labels: + app: finmind + managed-by: gcloud + annotations: + run.googleapis.com/launch-stage: GA + run.googleapis.com/ingress: all +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "1" + autoscaling.knative.dev/maxScale: "10" + run.googleapis.com/cpu-throttling: "false" + run.googleapis.com/execution-environment: gen2 + run.googleapis.com/cloudsql-instances: PROJECT_ID:REGION:finmind-db + spec: + containerConcurrency: 80 + timeoutSeconds: 300 + serviceAccountName: finmind-sa@PROJECT_ID.iam.gserviceaccount.com + containers: + - image: ghcr.io/rohitdash08/finmind-backend:latest + name: backend + ports: + - name: http1 + containerPort: 8000 + command: + - sh + - -c + - | + flask init-db + mkdir -p /tmp/prometheus_multiproc + export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc + exec gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:8000 wsgi:app + env: + - name: LOG_LEVEL + value: INFO + - name: GEMINI_MODEL + value: gemini-1.5-flash + - name: PORT + value: "8000" + # Secrets from Secret Manager + - name: DATABASE_URL + valueFrom: + secretKeyRef: + key: latest + name: finmind-database-url + - name: REDIS_URL + valueFrom: + secretKeyRef: + key: latest + name: finmind-redis-url + - name: JWT_SECRET + valueFrom: + secretKeyRef: + key: latest + name: finmind-jwt-secret + - name: GEMINI_API_KEY + valueFrom: + secretKeyRef: + key: latest + name: finmind-gemini-api-key + resources: + limits: + cpu: "1" + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + startupProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 20 + traffic: + - percent: 100 + latestRevision: true +--- +# Frontend Cloud Run service +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: finmind-frontend + namespace: PROJECT_ID + labels: + app: finmind + annotations: + run.googleapis.com/ingress: all +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "1" + autoscaling.knative.dev/maxScale: "5" + spec: + containers: + - image: ghcr.io/rohitdash08/finmind-frontend:latest + name: frontend + ports: + - name: http1 + containerPort: 80 + resources: + limits: + cpu: "0.5" + memory: 128Mi + traffic: + - percent: 100 + latestRevision: true diff --git a/deploy/platforms/heroku/Procfile b/deploy/platforms/heroku/Procfile new file mode 100644 index 00000000..0e2767ac --- /dev/null +++ b/deploy/platforms/heroku/Procfile @@ -0,0 +1,2 @@ +web: cd packages/backend && flask init-db && gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:$PORT wsgi:app +release: cd packages/backend && flask init-db diff --git a/deploy/platforms/heroku/app.json b/deploy/platforms/heroku/app.json new file mode 100644 index 00000000..ba98b231 --- /dev/null +++ b/deploy/platforms/heroku/app.json @@ -0,0 +1,60 @@ +{ + "name": "FinMind", + "description": "AI-powered personal finance manager — Flask + React + PostgreSQL + Redis", + "repository": "https://github.com/rohitdash08/FinMind", + "logo": "", + "keywords": ["finance", "flask", "react", "ai"], + "stack": "container", + + "buildpacks": [], + + "env": { + "LOG_LEVEL": { + "description": "Logging level (DEBUG, INFO, WARNING, ERROR)", + "value": "INFO" + }, + "GEMINI_MODEL": { + "description": "Google Gemini model to use for AI features", + "value": "gemini-1.5-flash" + }, + "JWT_SECRET": { + "description": "Secret key for JWT token signing (auto-generated)", + "generator": "secret" + }, + "GEMINI_API_KEY": { + "description": "Google Gemini API key for AI features", + "required": false + } + }, + + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + }, + + "addons": [ + { + "plan": "heroku-postgresql:essential-0", + "as": "DATABASE" + }, + { + "plan": "heroku-redis:mini", + "as": "REDIS" + } + ], + + "scripts": { + "postdeploy": "cd packages/backend && flask init-db" + }, + + "environments": { + "test": { + "addons": ["heroku-postgresql:essential-0"], + "scripts": { + "test": "cd packages/backend && python -m pytest tests/ -q" + } + } + } +} diff --git a/deploy/platforms/netlify/netlify.toml b/deploy/platforms/netlify/netlify.toml new file mode 100644 index 00000000..ca311861 --- /dev/null +++ b/deploy/platforms/netlify/netlify.toml @@ -0,0 +1,58 @@ +# FinMind — Netlify configuration (frontend static site) +# Docs: https://docs.netlify.com/configure-builds/file-based-configuration/ + +[build] + base = "app" + command = "npm ci && npm run build" + publish = "dist" + +[build.environment] + NODE_VERSION = "20" + NPM_VERSION = "10" + +# API proxy — avoids CORS and hides the backend URL +[[redirects]] + from = "/api/*" + to = "https://finmind-backend.your-domain.com/:splat" + status = 200 + force = true + + [redirects.headers] + X-From = "netlify" + +# SPA fallback — all routes go to index.html +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +# Security headers +[[headers]] + for = "/*" + + [headers.values] + X-Frame-Options = "DENY" + X-XSS-Protection = "1; mode=block" + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" + Permissions-Policy = "camera=(), microphone=(), geolocation=()" + Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' https://finmind-backend.your-domain.com" + +# Cache static assets aggressively +[[headers]] + for = "/assets/*" + + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +[context.production] + command = "npm ci && npm run build" + + [context.production.environment] + VITE_ENV = "production" + +[context.deploy-preview] + command = "npm ci && npm run build" + + [context.deploy-preview.environment] + VITE_ENV = "preview" diff --git a/deploy/platforms/railway/railway.toml b/deploy/platforms/railway/railway.toml new file mode 100644 index 00000000..5701c6f8 --- /dev/null +++ b/deploy/platforms/railway/railway.toml @@ -0,0 +1,43 @@ +# FinMind — Railway deployment configuration +# Docs: https://docs.railway.app/reference/railway-toml + +[build] +builder = "dockerfile" +dockerfilePath = "packages/backend/Dockerfile" + +[deploy] +startCommand = "gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:$PORT wsgi:app" +healthcheckPath = "/health" +healthcheckTimeout = 30 +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 3 + +[[services]] +name = "backend" +source = "packages/backend" + + [services.build] + builder = "dockerfile" + dockerfilePath = "packages/backend/Dockerfile" + + [services.deploy] + startCommand = "flask init-db && gunicorn -w 2 -k gthread --threads 4 -b 0.0.0.0:$PORT wsgi:app" + healthcheckPath = "/health" + + [[services.variables]] + # These are set in the Railway dashboard under Variables + # DATABASE_URL — provided by Railway PostgreSQL plugin + # REDIS_URL — provided by Railway Redis plugin + # JWT_SECRET — set manually + # GEMINI_API_KEY — set manually + +[[services]] +name = "frontend" +source = "app" + + [services.build] + builder = "dockerfile" + dockerfilePath = "app/Dockerfile" + + [services.deploy] + healthcheckPath = "/" diff --git a/deploy/platforms/render/render.yaml b/deploy/platforms/render/render.yaml new file mode 100644 index 00000000..a8aae788 --- /dev/null +++ b/deploy/platforms/render/render.yaml @@ -0,0 +1,79 @@ +# FinMind — Render Blueprint +# Deploy: https://render.com/deploy (point to this file) +# Docs: https://render.com/docs/blueprint-spec + +services: + # ------------------------------------------------------------------------- + # Backend API + # ------------------------------------------------------------------------- + - type: web + name: finmind-backend + runtime: docker + dockerfilePath: ./packages/backend/Dockerfile + dockerContext: ./packages/backend + plan: starter + region: oregon + healthCheckPath: /health + envVars: + - key: DATABASE_URL + fromDatabase: + name: finmind-db + property: connectionString + - key: REDIS_URL + fromService: + type: redis + name: finmind-redis + property: connectionString + - key: JWT_SECRET + generateValue: true + - key: GEMINI_API_KEY + sync: false # set in Render dashboard + - key: LOG_LEVEL + value: INFO + - key: GEMINI_MODEL + value: gemini-1.5-flash + - key: PORT + value: "8000" + scaling: + minInstances: 1 + maxInstances: 3 + targetMemoryPercent: 80 + targetCPUPercent: 70 + + # ------------------------------------------------------------------------- + # Frontend (static site build) + # ------------------------------------------------------------------------- + - type: web + name: finmind-frontend + runtime: static + buildCommand: cd app && npm ci && npm run build + staticPublishPath: ./app/dist + envVars: + - key: VITE_API_URL + fromService: + type: web + name: finmind-backend + property: host + routes: + - type: rewrite + source: /* + destination: /index.html + +# ------------------------------------------------------------------------- +# PostgreSQL database +# ------------------------------------------------------------------------- +databases: + - name: finmind-db + databaseName: finmind + user: finmind + plan: starter + region: oregon + +# ------------------------------------------------------------------------- +# Redis cache +# ------------------------------------------------------------------------- + - name: finmind-redis + type: redis + plan: starter + region: oregon + maxmemoryPolicy: allkeys-lru diff --git a/deploy/platforms/vercel/vercel.json b/deploy/platforms/vercel/vercel.json new file mode 100644 index 00000000..9f6c2405 --- /dev/null +++ b/deploy/platforms/vercel/vercel.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "version": 2, + "name": "finmind-frontend", + "framework": "vite", + "buildCommand": "npm run build", + "outputDirectory": "dist", + "installCommand": "npm ci", + "devCommand": "npm run dev", + "rootDirectory": "app", + + "rewrites": [ + { + "source": "/api/:path*", + "destination": "https://finmind-backend.your-domain.com/api/:path*" + }, + { + "source": "/((?!api/).*)", + "destination": "/index.html" + } + ], + + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-Frame-Options", "value": "DENY" }, + { "key": "X-XSS-Protection", "value": "1; mode=block" }, + { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" } + ] + }, + { + "source": "/assets/(.*)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ], + + "env": { + "VITE_API_URL": "@finmind-api-url" + }, + + "build": { + "env": { + "VITE_API_URL": "@finmind-api-url" + } + }, + + "regions": ["iad1"] +} diff --git a/deploy/tilt/Tiltfile b/deploy/tilt/Tiltfile new file mode 100644 index 00000000..29457ed6 --- /dev/null +++ b/deploy/tilt/Tiltfile @@ -0,0 +1,130 @@ +# ============================================================================= +# FinMind — Tiltfile for local Kubernetes development +# Usage: tilt up (requires: tilt, kubectl, helm) +# ============================================================================= + +# --------------------------------------------------------------------------- +# Settings +# --------------------------------------------------------------------------- +NAMESPACE = "finmind" +BACKEND_PORT = 8000 +FRONTEND_PORT = 5173 +GRAFANA_PORT = 3000 + +# Load Helm extension +load('ext://helm_resource', 'helm_resource', 'helm_repo') +load('ext://namespace', 'namespace_create', 'namespace_inject') + +# Ensure namespace exists +namespace_create(NAMESPACE) + +# --------------------------------------------------------------------------- +# Build images locally with hot-reload +# --------------------------------------------------------------------------- +docker_build( + 'ghcr.io/rohitdash08/finmind-backend', + context='../../packages/backend', + dockerfile='../../packages/backend/Dockerfile', + live_update=[ + # Sync Python source files without rebuilding + sync('../../packages/backend/app', '/app/app'), + sync('../../packages/backend/wsgi.py', '/app/wsgi.py'), + # Reinstall deps if requirements change + run( + 'pip install -r /app/requirements.txt', + trigger=['../../packages/backend/requirements.txt'], + ), + # Restart gunicorn after code changes + run('kill -HUP $(cat /tmp/gunicorn.pid) 2>/dev/null || true'), + ], +) + +docker_build( + 'ghcr.io/rohitdash08/finmind-frontend', + context='../../app', + dockerfile='../../app/Dockerfile', + target='builder', # use the build stage for dev (hot-reload via port-forward) + live_update=[ + sync('../../app/src', '/app/src'), + sync('../../app/public', '/app/public'), + run( + 'npm install', + trigger=['../../app/package.json', '../../app/package-lock.json'], + ), + ], +) + +# --------------------------------------------------------------------------- +# Deploy via Helm (uses locally built images) +# --------------------------------------------------------------------------- +helm_resource( + 'finmind', + chart='../k8s/helm', + namespace=NAMESPACE, + flags=[ + '--set', 'global.imagePullPolicy=Never', # use locally built images + '--set', 'ingress.enabled=false', # use port-forwards in dev + '--set', 'secrets.create=true', + '--values', '../k8s/helm/values.yaml', + ], + image_deps=[ + 'ghcr.io/rohitdash08/finmind-backend', + 'ghcr.io/rohitdash08/finmind-frontend', + ], + image_keys=[ + ('backend.image.repository', 'backend.image.tag'), + ('frontend.image.repository', 'frontend.image.tag'), + ], +) + +# --------------------------------------------------------------------------- +# Port forwards for local access +# --------------------------------------------------------------------------- +k8s_resource( + 'finmind', + port_forwards=[ + port_forward(BACKEND_PORT, BACKEND_PORT, name='backend-api'), + port_forward(FRONTEND_PORT, 80, name='frontend'), + port_forward(GRAFANA_PORT, GRAFANA_PORT, name='grafana'), + port_forward(9090, 9090, name='prometheus'), + ], +) + +# --------------------------------------------------------------------------- +# Local resource: run backend tests on file change +# --------------------------------------------------------------------------- +local_resource( + 'backend-tests', + cmd='cd ../../packages/backend && python -m pytest tests/ -q --tb=short 2>&1 | tail -20', + deps=['../../packages/backend/app', '../../packages/backend/tests'], + trigger_mode=TRIGGER_MODE_MANUAL, + labels=['tests'], +) + +# --------------------------------------------------------------------------- +# Local resource: lint frontend +# --------------------------------------------------------------------------- +local_resource( + 'frontend-lint', + cmd='cd ../../app && npm run lint 2>&1 | tail -20', + deps=['../../app/src'], + trigger_mode=TRIGGER_MODE_MANUAL, + labels=['tests'], +) + +# --------------------------------------------------------------------------- +# Useful links shown in Tilt UI +# --------------------------------------------------------------------------- +config.define_string_list('to-run', args=True) +cfg = config.parse() + +print(""" +╔══════════════════════════════════════════╗ +║ FinMind — Tilt Dev Environment Ready ║ +╠══════════════════════════════════════════╣ +║ Backend API : http://localhost:8000 ║ +║ Frontend : http://localhost:5173 ║ +║ Grafana : http://localhost:3000 ║ +║ Prometheus : http://localhost:9090 ║ +╚══════════════════════════════════════════╝ +""") diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 00000000..ed9f406a --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,552 @@ +# FinMind Deployment Guide + +Production-grade, one-click deployments for every major platform. + +--- + +## Quick Start + +```bash +# Clone the repo +git clone https://github.com/rohitdash08/FinMind && cd FinMind + +# Deploy locally with Docker Compose (no setup required) +./scripts/deploy.sh docker + +# Deploy to any cloud in one command +./scripts/deploy.sh [platform] +``` + +### All supported platforms + +| Platform | Command | Type | +|---|---|---| +| Docker Compose | `./scripts/deploy.sh docker` | Local | +| Kubernetes (raw) | `./scripts/deploy.sh k8s` | K8s | +| Kubernetes (Helm) | `./scripts/deploy.sh helm` | K8s | +| Tilt (local K8s dev) | `./scripts/deploy.sh tilt` | Local K8s | +| Railway | `./scripts/deploy.sh railway` | PaaS | +| Render | `./scripts/deploy.sh render` | PaaS | +| Fly.io | `./scripts/deploy.sh fly` | PaaS | +| Heroku | `./scripts/deploy.sh heroku` | PaaS | +| DigitalOcean App Platform | `./scripts/deploy.sh digitalocean` | PaaS | +| AWS ECS/Fargate | `./scripts/deploy.sh aws` | Cloud | +| Google Cloud Run | `./scripts/deploy.sh gcp` | Cloud | +| Azure Container Apps | `./scripts/deploy.sh azure` | Cloud | +| Netlify (frontend) | `./scripts/deploy.sh netlify` | Static | +| Vercel (frontend) | `./scripts/deploy.sh vercel` | Static | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Internet │ +└────────────────────────┬────────────────────────────┘ + │ + ┌──────────▼──────────┐ + │ Nginx (proxy) │ :8080 + └──────┬──────┬───────┘ + │ │ + ┌────────────▼──┐ ┌─▼──────────────┐ + │ Backend API │ │ Frontend │ + │ Flask/Gunicorn│ │ React (Nginx) │ + │ :8000 │ │ :80 │ + └───────┬───────┘ └────────────────┘ + │ + ┌──────────┼──────────┐ + │ │ │ +┌────▼───┐ ┌───▼───┐ ┌────▼────┐ +│Postgres│ │ Redis │ │Prometheus│ +│ :5432 │ │ :6379 │ │ :9090 │ +└────────┘ └───────┘ └────┬────┘ + │ + ┌──────▼──────┐ + │ Grafana │ + │ :3000 │ + └─────────────┘ +``` + +--- + +## 1. Docker Compose + +**Prerequisites:** Docker ≥ 24, Docker Compose ≥ 2.20 + +### Setup (3 steps) + +```bash +# 1. Copy and configure environment +cp .env.example .env +# Edit .env: set POSTGRES_PASSWORD, JWT_SECRET, GEMINI_API_KEY + +# 2. Start the full stack +./scripts/deploy.sh docker +# or: docker compose up -d + +# 3. Verify +curl http://localhost:8080/health +``` + +**Access:** +- API: http://localhost:8080 +- Frontend: http://localhost:5173 +- Grafana: http://localhost:3000 (admin / change-me-admin-password) + +**Useful commands:** +```bash +docker compose logs -f backend # stream backend logs +docker compose exec backend flask shell # open Flask shell +docker compose down -v # stop and remove volumes +``` + +--- + +## 2. Kubernetes — Raw Manifests + +**Prerequisites:** kubectl, a running K8s cluster (minikube, kind, GKE, EKS, AKS…) + +### Setup (3 steps) + +```bash +# 1. Copy and edit secrets +cp deploy/k8s/secrets.example.yaml deploy/k8s/secrets.yaml +# Edit secrets.yaml — base64-encode all values: +# echo -n "mypassword" | base64 + +# 2. Apply all manifests +./scripts/deploy.sh k8s +# or manually: +kubectl apply -f deploy/k8s/namespace.yaml +kubectl apply -f deploy/k8s/secrets.yaml -n finmind +kubectl apply -f deploy/k8s/app-stack.yaml -n finmind +kubectl apply -f deploy/k8s/monitoring-stack.yaml -n finmind + +# 3. Verify +kubectl get pods -n finmind +kubectl rollout status deployment/backend -n finmind +``` + +**Port-forward for local access:** +```bash +kubectl port-forward svc/nginx 8080:8080 -n finmind & +kubectl port-forward svc/grafana 3000:3000 -n finmind & +``` + +--- + +## 3. Kubernetes — Helm + +**Prerequisites:** helm ≥ 3.12, kubectl + +### Setup (3 steps) + +```bash +# 1. Create namespace and install +kubectl create namespace finmind --dry-run=client -o yaml | kubectl apply -f - + +# 2. Install with your values +helm upgrade --install finmind deploy/k8s/helm \ + --namespace finmind \ + --set secrets.postgresPassword=$(echo -n "yourpassword" | base64) \ + --set secrets.jwtSecret=$(openssl rand -base64 32 | base64) \ + --set ingress.hosts.api=api.yourdomain.com \ + --set ingress.hosts.app=app.yourdomain.com \ + --wait + +# 3. Verify +helm status finmind -n finmind +kubectl get pods -n finmind +``` + +**Custom values file (recommended for production):** +```yaml +# my-prod-values.yaml +ingress: + hosts: + api: api.yourdomain.com + app: app.yourdomain.com + grafana: grafana.yourdomain.com + tls: + enabled: true +secrets: + postgresPassword: + jwtSecret: + geminiApiKey: +backend: + replicaCount: 3 + autoscaling: + enabled: true + maxReplicas: 10 +``` + +```bash +helm upgrade --install finmind deploy/k8s/helm \ + -f my-prod-values.yaml --namespace finmind --wait +``` + +**Helm chart structure:** +``` +deploy/k8s/helm/ +├── Chart.yaml # Chart metadata +├── values.yaml # Default values (fully documented) +└── templates/ + ├── configmap.yaml # App config + Nginx config + ├── secrets.yaml # K8s Secret (all credentials) + ├── pvc.yaml # PersistentVolumeClaims + ├── deployment.yaml # All Deployments (postgres, redis, backend, frontend, nginx) + ├── service.yaml # ClusterIP Services + ├── ingress.yaml # Ingress with TLS (cert-manager annotations) + └── hpa.yaml # HorizontalPodAutoscalers (backend + frontend) +``` + +--- + +## 4. Tilt — Local K8s Dev with Hot-Reload + +**Prerequisites:** tilt ≥ 0.33, kubectl, helm, Docker + +### Setup (3 steps) + +```bash +# 1. Start a local cluster (if needed) +minikube start # or: kind create cluster + +# 2. Launch Tilt +./scripts/deploy.sh tilt +# or: cd deploy/tilt && tilt up + +# 3. Open the Tilt UI +open http://localhost:10350 +``` + +**Features:** +- Live sync: Python/React source files sync into running containers without rebuild +- Auto-test: backend tests and frontend lint triggered on file change (manual trigger in UI) +- Port-forwards: backend :8000, frontend :5173, Grafana :3000, Prometheus :9090 + +--- + +## 5. Railway + +**Prerequisites:** Railway CLI (`npm i -g @railway/cli`), Railway account + +### Setup (3 steps) + +```bash +# 1. Login and link project +railway login +railway init # or: railway link + +# 2. Add secrets +railway variables set JWT_SECRET=$(openssl rand -hex 32) +railway variables set GEMINI_API_KEY=your-key + +# 3. Deploy +./scripts/deploy.sh railway +``` + +Railway auto-provisions PostgreSQL and Redis plugins. The `DATABASE_URL` and `REDIS_URL` are injected automatically. + +--- + +## 6. Render + +**Prerequisites:** Render account, GitHub repo connected + +### Setup (3 steps) + +```bash +# 1. One-click deploy via Blueprint +# Go to: https://render.com/deploy +# Paste your GitHub repo URL — Render reads deploy/platforms/render/render.yaml + +# 2. Set secrets in Render dashboard: +# JWT_SECRET, GEMINI_API_KEY + +# 3. Trigger deploy +git push origin main # auto-deploys on push +``` + +The Blueprint provisions: backend web service, frontend static site, PostgreSQL, Redis. + +--- + +## 7. Fly.io + +**Prerequisites:** `flyctl` CLI, Fly account + +### Setup (3 steps) + +```bash +# 1. Login and create app +flyctl auth login +flyctl apps create finmind-backend --org personal + +# 2. Set secrets +flyctl secrets set \ + DATABASE_URL="postgresql://..." \ + REDIS_URL="redis://..." \ + JWT_SECRET="$(openssl rand -hex 32)" \ + GEMINI_API_KEY="your-key" + +# 3. Deploy +./scripts/deploy.sh fly +# or: flyctl deploy --config deploy/platforms/fly/fly.toml +``` + +**Scale:** +```bash +flyctl scale count 3 # run 3 machines +flyctl scale vm shared-cpu-2x # upgrade machine size +``` + +--- + +## 8. Heroku + +**Prerequisites:** Heroku CLI, Heroku account, git remote configured + +### Setup (3 steps) + +```bash +# 1. Create app and add-ons +heroku create finmind +heroku addons:create heroku-postgresql:essential-0 +heroku addons:create heroku-redis:mini + +# 2. Set config vars +heroku config:set \ + JWT_SECRET=$(openssl rand -hex 32) \ + GEMINI_API_KEY=your-key \ + LOG_LEVEL=INFO + +# 3. Deploy +./scripts/deploy.sh heroku +# or: git push heroku main +``` + +The `Procfile` runs `flask init-db` as a release phase command automatically. + +--- + +## 9. DigitalOcean App Platform + +**Prerequisites:** `doctl` CLI, DigitalOcean account + +### Setup (3 steps) + +```bash +# 1. Authenticate +doctl auth init + +# 2. Set secrets in the spec or via dashboard: +# JWT_SECRET, GEMINI_API_KEY + +# 3. Deploy +./scripts/deploy.sh digitalocean +# or: doctl apps create --spec deploy/platforms/digitalocean/app.yaml +``` + +The app spec provisions: backend service, frontend static site, managed PostgreSQL 16, managed Redis 7. + +--- + +## 10. AWS ECS / Fargate + +**Prerequisites:** AWS CLI v2, configured credentials (`aws configure`), existing ECS cluster + +### Setup (3 steps) + +```bash +# 1. Create secrets in AWS Secrets Manager +aws secretsmanager create-secret --name finmind/DATABASE_URL --secret-string "postgresql://..." +aws secretsmanager create-secret --name finmind/REDIS_URL --secret-string "redis://..." +aws secretsmanager create-secret --name finmind/JWT_SECRET --secret-string "$(openssl rand -hex 32)" +aws secretsmanager create-secret --name finmind/GEMINI_API_KEY --secret-string "your-key" + +# 2. Edit task definition — replace ACCOUNT_ID and REGION placeholders +sed -i 's/ACCOUNT_ID/123456789012/g; s/REGION/us-east-1/g' \ + deploy/platforms/aws/task-definition.json + +# 3. Register and deploy +export AWS_ECS_CLUSTER=finmind AWS_ECS_SERVICE=finmind-backend AWS_REGION=us-east-1 +./scripts/deploy.sh aws +``` + +--- + +## 11. Google Cloud Run + +**Prerequisites:** `gcloud` CLI, GCP project with billing enabled + +### Setup (3 steps) + +```bash +# 1. Authenticate and set project +gcloud auth login +gcloud config set project YOUR_PROJECT_ID + +# 2. Store secrets in Secret Manager +gcloud secrets create finmind-database-url --data-file=- <<< "postgresql://..." +gcloud secrets create finmind-redis-url --data-file=- <<< "redis://..." +gcloud secrets create finmind-jwt-secret --data-file=- <<< "$(openssl rand -hex 32)" +gcloud secrets create finmind-gemini-api-key --data-file=- <<< "your-key" + +# Grant Cloud Run SA access to secrets +gcloud secrets add-iam-policy-binding finmind-jwt-secret \ + --member="serviceAccount:finmind-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/secretmanager.secretAccessor" + +# 3. Deploy +export GCP_PROJECT_ID=YOUR_PROJECT_ID GCP_REGION=us-east1 +./scripts/deploy.sh gcp +``` + +--- + +## 12. Azure Container Apps + +**Prerequisites:** Azure CLI (`az`), Azure subscription + +### Setup (3 steps) + +```bash +# 1. Login and create resource group +az login +az group create -n finmind-rg --location eastus +az containerapp env create -n finmind-env -g finmind-rg --location eastus + +# 2. Store secrets in Key Vault +az keyvault create -n finmind-kv -g finmind-rg --location eastus +az keyvault secret set --vault-name finmind-kv -n DATABASE-URL --value "postgresql://..." +az keyvault secret set --vault-name finmind-kv -n REDIS-URL --value "redis://..." +az keyvault secret set --vault-name finmind-kv -n JWT-SECRET --value "$(openssl rand -hex 32)" +az keyvault secret set --vault-name finmind-kv -n GEMINI-API-KEY --value "your-key" + +# 3. Deploy +export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) +./scripts/deploy.sh azure +``` + +--- + +## 13. Netlify (Frontend) + +**Prerequisites:** Netlify CLI (`npm i -g netlify-cli`), Netlify account + +### Setup (3 steps) + +```bash +# 1. Login +netlify login + +# 2. Set backend URL env var in Netlify dashboard: +# VITE_API_URL = https://your-backend-url + +# 3. Deploy +./scripts/deploy.sh netlify +# or: cd app && netlify deploy --prod +``` + +The `netlify.toml` configures SPA routing, API proxy, security headers, and asset caching. + +--- + +## 14. Vercel (Frontend) + +**Prerequisites:** Vercel CLI (`npm i -g vercel`), Vercel account + +### Setup (3 steps) + +```bash +# 1. Login +vercel login + +# 2. Set secret in Vercel dashboard or CLI: +vercel env add VITE_API_URL production # enter your backend URL + +# 3. Deploy +./scripts/deploy.sh vercel +# or: cd app && vercel --prod +``` + +--- + +## Environment Variables Reference + +| Variable | Required | Default | Description | +|---|---|---|---| +| `DATABASE_URL` | Yes | — | PostgreSQL connection string | +| `REDIS_URL` | Yes | — | Redis connection string | +| `JWT_SECRET` | Yes | — | Secret for signing JWT tokens (≥32 chars) | +| `GEMINI_API_KEY` | No | — | Google Gemini API key for AI features | +| `GEMINI_MODEL` | No | `gemini-1.5-flash` | Gemini model name | +| `LOG_LEVEL` | No | `INFO` | Logging level (DEBUG/INFO/WARNING/ERROR) | +| `PORT` | No | `8000` | Backend HTTP port (set by platform) | +| `PROMETHEUS_MULTIPROC_DIR` | No | `/tmp/prometheus_multiproc` | Prometheus multiprocess dir | + +--- + +## Monitoring + +All deployments include Prometheus + Grafana + Loki (where supported). + +| Service | URL | Default Credentials | +|---|---|---| +| Grafana | http://localhost:3000 (Docker) | admin / change-me-admin-password | +| Prometheus | http://localhost:9090 (Docker) | — | +| Backend metrics | http://localhost:8000/metrics | — | + +**Scraped targets:** +- Backend application (`/metrics`) +- PostgreSQL (`postgres-exporter`) +- Redis (`redis-exporter`) +- Nginx (`nginx-exporter`) +- Node metrics (`node-exporter`) + +--- + +## Secrets Generation + +```bash +# Generate strong secrets for production +JWT_SECRET=$(openssl rand -hex 32) +POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/') + +# Base64-encode for Kubernetes secrets +echo -n "$JWT_SECRET" | base64 +echo -n "$POSTGRES_PASSWORD" | base64 +``` + +--- + +## Troubleshooting + +**Backend not starting:** +```bash +docker compose logs backend # Docker +kubectl logs -l app=backend -n finmind # K8s +``` + +**Database migration failed:** +```bash +docker compose exec backend flask init-db +kubectl exec -it deploy/backend -n finmind -- flask init-db +``` + +**Health check failing:** +```bash +curl http://localhost:8080/health # should return {"status": "ok"} +``` + +**Helm dry-run:** +```bash +helm upgrade --install finmind deploy/k8s/helm --dry-run --debug -n finmind +``` + +**Deploy script dry-run:** +```bash +./scripts/deploy.sh helm --dry-run +``` diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..680981c8 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,365 @@ +#!/usr/bin/env bash +# ============================================================================= +# FinMind — Universal One-Click Deployment Script +# ============================================================================= +# Usage: +# ./scripts/deploy.sh [platform] [options] +# +# Platforms: +# docker Local Docker Compose (default) +# k8s Kubernetes with raw manifests +# helm Kubernetes with Helm chart +# tilt Local K8s dev with Tilt hot-reload +# railway Railway.app +# render Render.com +# fly Fly.io +# heroku Heroku +# digitalocean DigitalOcean App Platform +# aws AWS ECS/Fargate +# gcp Google Cloud Run +# azure Azure Container Apps +# netlify Netlify (frontend only) +# vercel Vercel (frontend only) +# +# Options: +# --env ENV Target environment (dev|staging|prod) [default: prod] +# --tag TAG Docker image tag [default: latest] +# --dry-run Print commands without executing +# --help Show this help +# +# Examples: +# ./scripts/deploy.sh docker +# ./scripts/deploy.sh helm --env prod --tag v1.2.3 +# ./scripts/deploy.sh fly --dry-run +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Colours +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +RESET='\033[0m' + +log() { echo -e "${BLUE}[finmind]${RESET} $*"; } +ok() { echo -e "${GREEN}[✓]${RESET} $*"; } +warn() { echo -e "${YELLOW}[!]${RESET} $*"; } +die() { echo -e "${RED}[✗]${RESET} $*" >&2; exit 1; } + +# --------------------------------------------------------------------------- +# Defaults +# --------------------------------------------------------------------------- +PLATFORM="${1:-docker}" +ENV="prod" +TAG="latest" +DRY_RUN=false +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- +shift 1 2>/dev/null || true +while [[ $# -gt 0 ]]; do + case "$1" in + --env) ENV="$2"; shift 2 ;; + --tag) TAG="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --help|-h) sed -n '3,40p' "$0"; exit 0 ;; + *) die "Unknown option: $1. Run with --help for usage." ;; + esac +done + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +run() { + if $DRY_RUN; then + echo -e "${YELLOW}[dry-run]${RESET} $*" + else + eval "$*" + fi +} + +require() { + for cmd in "$@"; do + command -v "$cmd" &>/dev/null || die "'$cmd' is required but not installed." + done +} + +check_env_var() { + local var="$1" + [[ -n "${!var:-}" ]] || die "Environment variable '$var' is not set." +} + +banner() { + echo "" + echo -e "${BOLD}╔══════════════════════════════════════════════╗${RESET}" + echo -e "${BOLD}║ FinMind Deployment — ${PLATFORM^^} / ${ENV^^}$(printf '%*s' $((18 - ${#PLATFORM} - ${#ENV})) '')║${RESET}" + echo -e "${BOLD}╚══════════════════════════════════════════════╝${RESET}" + echo "" +} + +# --------------------------------------------------------------------------- +# Platform handlers +# --------------------------------------------------------------------------- + +deploy_docker() { + require docker + log "Starting Docker Compose stack (env=$ENV, tag=$TAG)..." + cd "$REPO_ROOT" + + if [[ "$ENV" == "dev" ]]; then + run "BACKEND_TAG=$TAG FRONTEND_TAG=$TAG docker compose up --build -d" + else + run "BACKEND_TAG=$TAG FRONTEND_TAG=$TAG docker compose up -d --pull always" + fi + + ok "Docker stack is up. Services:" + run "docker compose ps" + echo "" + echo " API: http://localhost:8080" + echo " Frontend: http://localhost:5173" + echo " Grafana: http://localhost:3000" +} + +deploy_k8s() { + require kubectl + log "Deploying to Kubernetes with raw manifests..." + cd "$REPO_ROOT/deploy/k8s" + + run "kubectl apply -f namespace.yaml" + run "kubectl apply -f secrets.example.yaml -n finmind" && \ + warn "Applied example secrets — update them with real values immediately!" + run "kubectl apply -f app-stack.yaml -n finmind" + run "kubectl apply -f monitoring-stack.yaml -n finmind" + run "kubectl rollout status deployment/backend -n finmind --timeout=120s" + + ok "Kubernetes manifests applied." +} + +deploy_helm() { + require helm kubectl + log "Deploying to Kubernetes with Helm (tag=$TAG)..." + cd "$REPO_ROOT" + + run "kubectl apply -f deploy/k8s/namespace.yaml" + run "helm upgrade --install finmind deploy/k8s/helm \ + --namespace finmind \ + --set backend.image.tag=$TAG \ + --set frontend.image.tag=$TAG \ + --atomic \ + --timeout 5m \ + --wait" + + ok "Helm release 'finmind' deployed." + run "helm status finmind -n finmind" +} + +deploy_tilt() { + require tilt + log "Starting Tilt local K8s dev environment..." + cd "$REPO_ROOT/deploy/tilt" + run "tilt up" +} + +deploy_railway() { + require railway + log "Deploying to Railway..." + cd "$REPO_ROOT" + run "railway up --config deploy/platforms/railway/railway.toml" + ok "Railway deployment triggered." +} + +deploy_render() { + log "Deploying to Render..." + warn "Render deploys automatically on git push when connected to GitHub." + warn "For manual deploy: https://dashboard.render.com → Manual Deploy" + echo "" + echo " Blueprint spec: deploy/platforms/render/render.yaml" + echo " Import URL: https://render.com/deploy?repo=https://github.com/rohitdash08/FinMind" +} + +deploy_fly() { + require flyctl + log "Deploying to Fly.io (tag=$TAG)..." + cd "$REPO_ROOT" + + # Ensure secrets are set + warn "Ensure these secrets are set: fly secrets set DATABASE_URL=... REDIS_URL=... JWT_SECRET=... GEMINI_API_KEY=..." + + run "flyctl deploy \ + --config deploy/platforms/fly/fly.toml \ + --image ghcr.io/rohitdash08/finmind-backend:$TAG \ + --remote-only" + + ok "Fly.io deployment complete." + run "flyctl status --config deploy/platforms/fly/fly.toml" +} + +deploy_heroku() { + require heroku git + log "Deploying to Heroku..." + cd "$REPO_ROOT" + + APP_NAME="${HEROKU_APP_NAME:-finmind}" + run "heroku stack:set container -a $APP_NAME" + run "heroku config:set LOG_LEVEL=INFO GEMINI_MODEL=gemini-1.5-flash -a $APP_NAME" + warn "Set secrets: heroku config:set JWT_SECRET=... GEMINI_API_KEY=... -a $APP_NAME" + run "git push heroku main" + + ok "Heroku deployment pushed." +} + +deploy_digitalocean() { + require doctl + log "Deploying to DigitalOcean App Platform..." + cd "$REPO_ROOT" + + if doctl apps list --format ID,Spec.Name --no-header 2>/dev/null | grep -q finmind; then + APP_ID=$(doctl apps list --format ID,Spec.Name --no-header | grep finmind | awk '{print $1}') + run "doctl apps update $APP_ID --spec deploy/platforms/digitalocean/app.yaml" + ok "DigitalOcean app updated: $APP_ID" + else + run "doctl apps create --spec deploy/platforms/digitalocean/app.yaml" + ok "DigitalOcean app created." + fi +} + +deploy_aws() { + require aws + log "Deploying to AWS ECS/Fargate..." + cd "$REPO_ROOT" + + CLUSTER="${AWS_ECS_CLUSTER:-finmind}" + SERVICE="${AWS_ECS_SERVICE:-finmind-backend}" + REGION="${AWS_REGION:-us-east-1}" + + # Register updated task definition + TASK_DEF_ARN=$(run "aws ecs register-task-definition \ + --cli-input-json file://deploy/platforms/aws/task-definition.json \ + --region $REGION \ + --query 'taskDefinition.taskDefinitionArn' \ + --output text") + + # Update service to use new task definition + run "aws ecs update-service \ + --cluster $CLUSTER \ + --service $SERVICE \ + --task-definition $TASK_DEF_ARN \ + --force-new-deployment \ + --region $REGION" + + ok "ECS service update triggered." + log "Waiting for service stability..." + run "aws ecs wait services-stable --cluster $CLUSTER --services $SERVICE --region $REGION" + ok "ECS service is stable." +} + +deploy_gcp() { + require gcloud + log "Deploying to Google Cloud Run..." + cd "$REPO_ROOT" + + PROJECT="${GCP_PROJECT_ID:?Set GCP_PROJECT_ID}" + REGION="${GCP_REGION:-us-east1}" + + # Replace placeholder PROJECT_ID + TMP_FILE=$(mktemp) + sed "s/PROJECT_ID/$PROJECT/g; s/REGION/$REGION/g" \ + deploy/platforms/gcp/cloudrun-service.yaml > "$TMP_FILE" + + run "gcloud run services replace $TMP_FILE --region $REGION --project $PROJECT" + rm -f "$TMP_FILE" + + ok "Cloud Run services updated." + run "gcloud run services list --region $REGION --project $PROJECT" +} + +deploy_azure() { + require az + log "Deploying to Azure Container Apps..." + cd "$REPO_ROOT" + + RG="${AZURE_RESOURCE_GROUP:-finmind-rg}" + ENV_NAME="${AZURE_ENV:-finmind-env}" + SUB="${AZURE_SUBSCRIPTION_ID:?Set AZURE_SUBSCRIPTION_ID}" + + # Ensure environment exists + run "az containerapp env show -n $ENV_NAME -g $RG &>/dev/null || \ + az containerapp env create -n $ENV_NAME -g $RG --location eastus" + + # Replace placeholder SUBSCRIPTION_ID + TMP_FILE=$(mktemp) + sed "s/SUBSCRIPTION_ID/$SUB/g" \ + deploy/platforms/azure/containerapps.yaml > "$TMP_FILE" + + run "az containerapp create \ + --yaml $TMP_FILE \ + --resource-group $RG \ + --subscription $SUB" + + rm -f "$TMP_FILE" + ok "Azure Container Apps deployed." +} + +deploy_netlify() { + require netlify + log "Deploying frontend to Netlify..." + cd "$REPO_ROOT/app" + + run "npm ci && npm run build" + run "netlify deploy --prod --dir dist --config ../deploy/platforms/netlify/netlify.toml" + + ok "Netlify deployment complete." +} + +deploy_vercel() { + require vercel + log "Deploying frontend to Vercel..." + cd "$REPO_ROOT/app" + + run "npm ci && npm run build" + if [[ "$ENV" == "prod" ]]; then + run "vercel --prod --yes" + else + run "vercel --yes" + fi + + ok "Vercel deployment complete." +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +banner + +log "Platform: ${BOLD}$PLATFORM${RESET} | Env: ${BOLD}$ENV${RESET} | Tag: ${BOLD}$TAG${RESET} | Dry-run: ${BOLD}$DRY_RUN${RESET}" +echo "" + +case "$PLATFORM" in + docker) deploy_docker ;; + k8s) deploy_k8s ;; + helm) deploy_helm ;; + tilt) deploy_tilt ;; + railway) deploy_railway ;; + render) deploy_render ;; + fly) deploy_fly ;; + heroku) deploy_heroku ;; + digitalocean) deploy_digitalocean ;; + aws) deploy_aws ;; + gcp) deploy_gcp ;; + azure) deploy_azure ;; + netlify) deploy_netlify ;; + vercel) deploy_vercel ;; + *) + die "Unknown platform '$PLATFORM'. +Run: ./scripts/deploy.sh --help for a list of supported platforms." + ;; +esac + +echo "" +ok "Done! FinMind deployed to ${PLATFORM}."