diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 00000000..99d880f1 --- /dev/null +++ b/Tiltfile @@ -0,0 +1,58 @@ +# -*- mode: Python -*- +# Tiltfile for FinMind local development + +# Load extensions +load('ext://restart_process', 'docker_build_with_restart') +load('ext://helm_resource', 'helm_resource', 'helm_repo') + +# Settings +allow_k8s_contexts(['docker-desktop', 'minikube', 'kind-finmind']) + +# Build backend image +docker_build( + 'finmind-backend', + context='.', + dockerfile='app/backend/Dockerfile', + live_update=[ + sync('./app/backend', '/app'), + run('pip install -r /app/requirements.txt', trigger=['./app/backend/requirements.txt']), + ] +) + +# Build frontend image +docker_build( + 'finmind-frontend', + context='.', + dockerfile='app/frontend/Dockerfile', + live_update=[ + sync('./app/frontend/src', '/app/src'), + run('npm install', trigger=['./app/frontend/package.json']), + ] +) + +# Deploy with Helm +helm_resource( + 'finmind', + chart='./deploy/helm', + namespace='finmind', + flags=[ + '--create-namespace', + '--values=./deploy/helm/values.yaml', + '--set=image.backend.tag=latest', + '--set=image.frontend.tag=latest', + ], + resource_deps=['finmind-backend', 'finmind-frontend'], + port_forwards=[ + '8000:8000', # Backend API + '3000:3000', # Frontend + ] +) + +# Health check +local_resource( + 'health-check', + cmd='curl -sf http://localhost:8000/api/health/ && echo "Backend OK"', + resource_deps=['finmind'], + auto_init=False, + trigger_mode=TRIGGER_MODE_MANUAL, +) diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 00000000..1a5f26b6 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# deploy.sh - Universal one-click deployment for FinMind +# Usage: ./deploy.sh [platform] +# Platforms: railway, heroku, render, fly, digitalocean, aws, gcp, azure, k8s, docker + +set -e + +PLATFORM="${1:-docker}" +APP_NAME="finmind" + +echo "FinMind Universal Deployment" +echo "Platform: $PLATFORM" +echo "================================" + +case "$PLATFORM" in + docker) + echo "Starting with Docker Compose..." + docker-compose up -d + echo "Done! Frontend: http://localhost:3000 | Backend: http://localhost:8000" + ;; + + k8s|kubernetes) + echo "Deploying to Kubernetes with Helm..." + kubectl create namespace finmind --dry-run=client -o yaml | kubectl apply -f - + helm upgrade --install finmind ./deploy/helm \ + --namespace finmind \ + --create-namespace \ + --values ./deploy/helm/values.yaml \ + --wait + echo "Done! Run: kubectl port-forward svc/finmind-frontend 3000:3000 -n finmind" + ;; + + railway) + echo "Deploying to Railway..." + command -v railway >/dev/null 2>&1 || npm install -g @railway/cli + railway login + railway up + echo "Done! Check Railway dashboard for URL." + ;; + + heroku) + echo "Deploying to Heroku..." + command -v heroku >/dev/null 2>&1 || curl https://cli-assets.heroku.com/install.sh | sh + heroku create $APP_NAME 2>/dev/null || true + heroku addons:create heroku-postgresql:mini -a $APP_NAME 2>/dev/null || true + heroku addons:create heroku-redis:mini -a $APP_NAME 2>/dev/null || true + heroku container:push web -a $APP_NAME + heroku container:release web -a $APP_NAME + echo "Done! URL: https://$APP_NAME.herokuapp.com" + ;; + + render) + echo "Deploying to Render..." + echo "Please connect your GitHub repo at https://render.com/new" + echo "Use render.yaml for automatic configuration." + ;; + + fly) + echo "Deploying to Fly.io..." + command -v flyctl >/dev/null 2>&1 || curl -L https://fly.io/install.sh | sh + flyctl launch --name $APP_NAME --no-deploy 2>/dev/null || true + flyctl deploy + echo "Done! URL: https://$APP_NAME.fly.dev" + ;; + + digitalocean|do) + echo "Deploying to DigitalOcean App Platform..." + command -v doctl >/dev/null 2>&1 || snap install doctl + doctl apps create --spec deploy/do-app-spec.yaml + echo "Done! Check DigitalOcean dashboard." + ;; + + aws) + echo "Deploying to AWS ECS Fargate..." + aws ecr get-login-password | docker login --username AWS --password-stdin \ + $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com + docker-compose -f deploy/docker-compose.aws.yml push + aws ecs update-service --cluster finmind --service finmind-backend --force-new-deployment + echo "Done! Check AWS ECS console." + ;; + + gcp) + echo "Deploying to GCP Cloud Run..." + gcloud builds submit --tag gcr.io/$(gcloud config get-value project)/finmind-backend ./app/backend + gcloud run deploy finmind-backend \ + --image gcr.io/$(gcloud config get-value project)/finmind-backend \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated + echo "Done! Check GCP Cloud Run console." + ;; + + azure) + echo "Deploying to Azure Container Apps..." + az containerapp up \ + --name $APP_NAME \ + --resource-group finmind-rg \ + --location eastus \ + --environment finmind-env \ + --image finmind-backend:latest \ + --target-port 8000 \ + --ingress external + echo "Done! Check Azure portal." + ;; + + tilt) + echo "Starting Tilt local dev environment..." + command -v tilt >/dev/null 2>&1 || curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash + tilt up + ;; + + *) + echo "Unknown platform: $PLATFORM" + echo "Available: docker, k8s, railway, heroku, render, fly, digitalocean, aws, gcp, azure, tilt" + exit 1 + ;; +esac diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml new file mode 100644 index 00000000..4109c541 --- /dev/null +++ b/deploy/helm/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: finmind +description: FinMind - Personal Finance Management Application +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - finance + - personal-finance + - budgeting +home: https://github.com/rohitdash08/FinMind +sources: + - https://github.com/rohitdash08/FinMind +maintainers: + - name: FinMind Team diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml new file mode 100644 index 00000000..71d37da5 --- /dev/null +++ b/deploy/helm/templates/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-backend + labels: + app: {{ .Release.Name }}-backend +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-backend + template: + metadata: + labels: + app: {{ .Release.Name }}-backend + spec: + containers: + - name: backend + image: "{{ .Values.image.backend.repository }}:{{ .Values.image.backend.tag }}" + imagePullPolicy: {{ .Values.image.backend.pullPolicy }} + ports: + - containerPort: 8000 + envFrom: + - secretRef: + name: {{ .Values.secrets.existingSecret }} + env: + - name: DEBUG + value: {{ .Values.env.DEBUG | quote }} + - name: ALLOWED_HOSTS + value: {{ .Values.env.ALLOWED_HOSTS | quote }} + resources: + {{- toYaml .Values.resources.backend | nindent 12 }} + livenessProbe: + httpGet: + path: /api/health/ + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/health/ + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-frontend + labels: + app: {{ .Release.Name }}-frontend +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-frontend + template: + metadata: + labels: + app: {{ .Release.Name }}-frontend + spec: + containers: + - name: frontend + image: "{{ .Values.image.frontend.repository }}:{{ .Values.image.frontend.tag }}" + imagePullPolicy: {{ .Values.image.frontend.pullPolicy }} + ports: + - containerPort: 3000 + resources: + {{- toYaml .Values.resources.frontend | nindent 12 }} + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 10 diff --git a/deploy/helm/templates/hpa.yaml b/deploy/helm/templates/hpa.yaml new file mode 100644 index 00000000..a92d6800 --- /dev/null +++ b/deploy/helm/templates/hpa.yaml @@ -0,0 +1,26 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-backend-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Release.Name }}-backend + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} +{{- end }} diff --git a/deploy/helm/templates/ingress.yaml b/deploy/helm/templates/ingress.yaml new file mode 100644 index 00000000..2d91a3d6 --- /dev/null +++ b/deploy/helm/templates/ingress.yaml @@ -0,0 +1,29 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ingress + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $.Release.Name }}-{{ .service }} + port: + number: {{ if eq .service "backend" }}{{ $.Values.service.backend.port }}{{ else }}{{ $.Values.service.frontend.port }}{{ end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/templates/service.yaml b/deploy/helm/templates/service.yaml new file mode 100644 index 00000000..6cafeee9 --- /dev/null +++ b/deploy/helm/templates/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-backend +spec: + type: {{ .Values.service.backend.type }} + selector: + app: {{ .Release.Name }}-backend + ports: + - port: {{ .Values.service.backend.port }} + targetPort: 8000 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-frontend +spec: + type: {{ .Values.service.frontend.type }} + selector: + app: {{ .Release.Name }}-frontend + ports: + - port: {{ .Values.service.frontend.port }} + targetPort: 3000 diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml new file mode 100644 index 00000000..8724fe3d --- /dev/null +++ b/deploy/helm/values.yaml @@ -0,0 +1,81 @@ +replicaCount: 1 + +image: + backend: + repository: finmind-backend + tag: latest + pullPolicy: IfNotPresent + frontend: + repository: finmind-frontend + tag: latest + pullPolicy: IfNotPresent + +service: + backend: + type: ClusterIP + port: 8000 + frontend: + type: ClusterIP + port: 3000 + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: finmind.example.com + paths: + - path: /api + pathType: Prefix + service: backend + - path: / + pathType: Prefix + service: frontend + tls: + - secretName: finmind-tls + hosts: + - finmind.example.com + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + +resources: + backend: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + frontend: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + +postgresql: + enabled: true + auth: + database: finmind + username: finmind + existingSecret: finmind-db-secret + +redis: + enabled: true + auth: + existingSecret: finmind-redis-secret + +env: + DEBUG: "false" + ALLOWED_HOSTS: "*" + CORS_ALLOWED_ORIGINS: "" + +secrets: + existingSecret: finmind-secrets diff --git a/fly.toml b/fly.toml new file mode 100644 index 00000000..2cd2b6eb --- /dev/null +++ b/fly.toml @@ -0,0 +1,26 @@ +app: finmind +primary_region: iad + +[build] + dockerfile = "app/backend/Dockerfile" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + + [http_service.concurrency] + type = "connections" + hard_limit = 25 + soft_limit = 20 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 + +[env] + DEBUG = "false" + ALLOWED_HOSTS = "*" diff --git a/render.yaml b/render.yaml new file mode 100644 index 00000000..3b4ef590 --- /dev/null +++ b/render.yaml @@ -0,0 +1,39 @@ +services: + - type: web + name: finmind-backend + runtime: docker + dockerfilePath: ./app/backend/Dockerfile + envVars: + - key: DEBUG + value: false + - key: DATABASE_URL + fromDatabase: + name: finmind-db + property: connectionString + - key: REDIS_URL + fromService: + type: redis + name: finmind-redis + property: connectionString + healthCheckPath: /api/health/ + + - type: web + name: finmind-frontend + runtime: docker + dockerfilePath: ./app/frontend/Dockerfile + envVars: + - key: NEXT_PUBLIC_API_URL + fromService: + type: web + name: finmind-backend + property: host + +databases: + - name: finmind-db + databaseName: finmind + user: finmind + plan: free + + - name: finmind-redis + type: redis + plan: free