diff --git a/.do/app.yaml b/.do/app.yaml new file mode 100644 index 00000000..2c803fd4 --- /dev/null +++ b/.do/app.yaml @@ -0,0 +1,77 @@ +spec: + name: finmind + region: nyc + services: + - name: backend + dockerfile_path: packages/backend/Dockerfile + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + http_port: 8000 + instance_count: 1 + instance_size_slug: basic-xxs + routes: + - path: /health + - path: /auth + - path: /expenses + - path: /bills + - path: /reminders + - path: /dashboard + - path: /insights + health_check: + http_path: /health + initial_delay_seconds: 15 + period_seconds: 10 + run_command: | + sh -c "python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app" + envs: + - key: DATABASE_URL + scope: RUN_TIME + value: ${db.DATABASE_URL} + - key: REDIS_URL + scope: RUN_TIME + type: SECRET + value: CHANGE_ME_SET_EXTERNAL_REDIS_URL + # DigitalOcean App Platform does not include managed Redis. + # Create a DO Managed Redis database or use Upstash (free tier), + # then replace this value in the App dashboard. + - key: JWT_SECRET + scope: RUN_TIME + type: SECRET + value: CHANGE_ME_SET_A_RANDOM_SECRET + - key: LOG_LEVEL + scope: RUN_TIME + value: INFO + - key: GEMINI_MODEL + scope: RUN_TIME + value: gemini-1.5-flash + + - name: frontend + dockerfile_path: app/Dockerfile + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + http_port: 80 + instance_count: 1 + instance_size_slug: basic-xxs + routes: + - path: / + envs: + - key: BACKEND_URL + scope: RUN_TIME + value: CHANGE_ME_SET_BACKEND_PUBLIC_URL + # Set this to the backend service's public URL after first deploy. + # Example: https://finmind-backend-xxxxx.ondigitalocean.app + + databases: + - name: db + engine: PG + version: "16" + size: db-s-dev-database + num_nodes: 1 + + # Note: DigitalOcean App Platform does not offer managed Redis. + # Create a DigitalOcean Managed Redis database separately, or use + # Upstash (free tier available), then set REDIS_URL on the backend. diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..dde34cc5 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:$PORT wsgi:app diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 00000000..5b5c121e --- /dev/null +++ b/Tiltfile @@ -0,0 +1,37 @@ +# FinMind Tiltfile — local Kubernetes development + +# Build backend image (matches K8s manifest image reference) +docker_build('ghcr.io/rohitdash08/finmind-backend', './packages/backend', + live_update=[ + sync('./packages/backend/app', '/app/app'), + sync('./packages/backend/wsgi.py', '/app/wsgi.py'), + run('pip install -r requirements.txt', trigger=['./packages/backend/requirements.txt']), + ] +) + +# Build frontend image for local development. +# The K8s manifests use a separate nginx reverse proxy; this image can be +# deployed manually or used when a frontend K8s Deployment is added. +docker_build('ghcr.io/rohitdash08/finmind-frontend', './app') + +# Apply K8s manifests +k8s_yaml([ + 'deploy/k8s/namespace.yaml', + 'deploy/k8s/secrets.example.yaml', + 'deploy/k8s/app-stack.yaml', +]) + +# Resource grouping and dependencies +k8s_resource('postgres', labels=['database'], + port_forwards='5432:5432') + +k8s_resource('redis', labels=['database'], + port_forwards='6379:6379') + +k8s_resource('backend', labels=['app'], + port_forwards='8000:8000', + resource_deps=['postgres', 'redis']) + +k8s_resource('nginx', labels=['app'], + port_forwards='8080:80', + resource_deps=['backend']) diff --git a/app.json b/app.json new file mode 100644 index 00000000..c129152a --- /dev/null +++ b/app.json @@ -0,0 +1,49 @@ +{ + "name": "FinMind", + "description": "AI-powered personal finance manager", + "repository": "https://github.com/rohitdash08/FinMind", + "logo": "", + "keywords": ["finance", "budgeting", "flask", "react"], + "stack": "container", + "addons": [ + { + "plan": "heroku-postgresql:essential-0" + }, + { + "plan": "heroku-redis:mini" + } + ], + "env": { + "JWT_SECRET": { + "description": "Secret key for JWT token signing", + "generator": "secret" + }, + "DATABASE_URL": { + "description": "PostgreSQL connection URL (auto-set by addon)" + }, + "REDIS_URL": { + "description": "Redis connection URL (auto-set by addon)" + }, + "LOG_LEVEL": { + "description": "Logging level", + "value": "INFO" + }, + "GEMINI_API_KEY": { + "description": "Google Gemini API key for AI features", + "required": false + }, + "GEMINI_MODEL": { + "description": "Gemini model name", + "value": "gemini-1.5-flash" + } + }, + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + }, + "buildpacks": [], + "scripts": {}, + "environments": {} +} diff --git a/app/.gitignore b/app/.gitignore index a547bf36..44854035 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Runtime config (generated at container start in dist/) +/dist/runtime-config.js diff --git a/app/Dockerfile b/app/Dockerfile index 2d1a3ad0..6e64b181 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -4,11 +4,17 @@ WORKDIR /app COPY package*.json ./ RUN npm ci || npm install COPY . . +ARG VITE_API_URL="" +ENV VITE_API_URL=${VITE_API_URL} RUN npm run build FROM nginx:alpine -# Copy custom nginx config for SPA fallback +# SPA nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf +# Built frontend assets COPY --from=builder /app/dist /usr/share/nginx/html +# Runtime config entrypoint (can override API URL at container start) +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/app/docker-entrypoint.sh b/app/docker-entrypoint.sh new file mode 100755 index 00000000..484a7974 --- /dev/null +++ b/app/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# Determine the backend API URL for client-side JavaScript. +# Priority: BACKEND_URL > VITE_API_URL > empty (relative / same-origin) +API_URL="${BACKEND_URL:-${VITE_API_URL:-}}" + +# Ensure URL has scheme if set +if [ -n "$API_URL" ]; then + case "$API_URL" in + http://*|https://*) ;; + *) API_URL="https://${API_URL}" ;; + esac +fi + +# Inject runtime config — the frontend reads window.__FINMIND_API_URL__ +CONFIG_FILE="/usr/share/nginx/html/runtime-config.js" +if [ -n "$API_URL" ]; then + echo "window.__FINMIND_API_URL__ = \"${API_URL}\";" > "$CONFIG_FILE" + echo "Runtime config: API_URL=${API_URL}" +else + echo "// No API URL configured — using build-time VITE_API_URL or default" > "$CONFIG_FILE" + echo "Warning: No BACKEND_URL or VITE_API_URL set." +fi + +exec nginx -g 'daemon off;' diff --git a/app/index.html b/app/index.html index a8dd1672..2e86733f 100644 --- a/app/index.html +++ b/app/index.html @@ -15,6 +15,8 @@
+ + diff --git a/app/public/runtime-config.js b/app/public/runtime-config.js new file mode 100644 index 00000000..ddfbeabf --- /dev/null +++ b/app/public/runtime-config.js @@ -0,0 +1,2 @@ +// Runtime configuration — generated by docker-entrypoint.sh in production. +// For local development, VITE_API_URL is set via .env or defaults to localhost:8000. diff --git a/deploy/aws/apprunner.yaml b/deploy/aws/apprunner.yaml new file mode 100644 index 00000000..5b00fed4 --- /dev/null +++ b/deploy/aws/apprunner.yaml @@ -0,0 +1,32 @@ +# AWS App Runner configuration +# Convert to JSON for the CLI, for example: +# yq -o=json deploy/aws/apprunner.yaml > /tmp/apprunner.json +# aws apprunner create-service --cli-input-json file:///tmp/apprunner.json +# +# This YAML is a reference. Convert to JSON for the CLI or use the console. + +ServiceName: finmind-backend +SourceConfiguration: + ImageRepository: + ImageIdentifier: + ImageRepositoryType: ECR + ImageConfiguration: + Port: "8000" + RuntimeEnvironmentVariables: + DATABASE_URL: + REDIS_URL: + JWT_SECRET: + LOG_LEVEL: INFO + GEMINI_MODEL: gemini-1.5-flash + StartCommand: "sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app'" + AutoDeploymentsEnabled: true +InstanceConfiguration: + Cpu: "0.25 vCPU" + Memory: "0.5 GB" +HealthCheckConfiguration: + Protocol: HTTP + Path: /health + Interval: 10 + Timeout: 5 + HealthyThreshold: 1 + UnhealthyThreshold: 5 diff --git a/deploy/aws/cloudformation.yaml b/deploy/aws/cloudformation.yaml new file mode 100644 index 00000000..2a50f6fc --- /dev/null +++ b/deploy/aws/cloudformation.yaml @@ -0,0 +1,178 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: FinMind ECS Fargate Stack + +Parameters: + VpcId: + Type: AWS::EC2::VPC::Id + SubnetIds: + Type: List + BackendImage: + Type: String + Description: Backend Docker image URI + FrontendImage: + Type: String + Description: Frontend Docker image URI + DatabaseUrl: + Type: String + NoEcho: true + RedisUrl: + Type: String + NoEcho: true + JwtSecret: + Type: String + NoEcho: true + MinLength: 32 + +Resources: + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: finmind + CapacityProviders: [FARGATE] + + ExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: finmind-execution-role + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + + TaskRole: + Type: AWS::IAM::Role + Properties: + RoleName: finmind-task-role + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /ecs/finmind + RetentionInDays: 14 + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: FinMind ECS Security Group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: 8000 + ToPort: 8000 + CidrIp: 0.0.0.0/0 + + BackendTaskDef: + Type: AWS::ECS::TaskDefinition + Properties: + Family: finmind-backend + Cpu: '256' + Memory: '512' + NetworkMode: awsvpc + RequiresCompatibilities: [FARGATE] + ExecutionRoleArn: !GetAtt ExecutionRole.Arn + TaskRoleArn: !GetAtt TaskRole.Arn + ContainerDefinitions: + - Name: backend + Image: !Ref BackendImage + Essential: true + PortMappings: + - ContainerPort: 8000 + Environment: + - Name: DATABASE_URL + Value: !Ref DatabaseUrl + - Name: REDIS_URL + Value: !Ref RedisUrl + - Name: JWT_SECRET + Value: !Ref JwtSecret + - Name: LOG_LEVEL + Value: INFO + Command: + - sh + - -c + - "python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: backend + HealthCheck: + Command: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] + Interval: 30 + Timeout: 5 + Retries: 3 + StartPeriod: 30 + + FrontendTaskDef: + Type: AWS::ECS::TaskDefinition + Properties: + Family: finmind-frontend + Cpu: '256' + Memory: '256' + NetworkMode: awsvpc + RequiresCompatibilities: [FARGATE] + ExecutionRoleArn: !GetAtt ExecutionRole.Arn + ContainerDefinitions: + - Name: frontend + Image: !Ref FrontendImage + Essential: true + PortMappings: + - ContainerPort: 80 + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: frontend + + BackendService: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref ECSCluster + ServiceName: finmind-backend + TaskDefinition: !Ref BackendTaskDef + DesiredCount: 1 + LaunchType: FARGATE + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: [!Ref SecurityGroup] + Subnets: !Ref SubnetIds + + FrontendService: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref ECSCluster + ServiceName: finmind-frontend + TaskDefinition: !Ref FrontendTaskDef + DesiredCount: 1 + LaunchType: FARGATE + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: [!Ref SecurityGroup] + Subnets: !Ref SubnetIds + +Outputs: + ClusterName: + Value: !Ref ECSCluster + BackendServiceName: + Value: !GetAtt BackendService.Name + FrontendServiceName: + Value: !GetAtt FrontendService.Name diff --git a/deploy/azure/main.bicep b/deploy/azure/main.bicep new file mode 100644 index 00000000..14384612 --- /dev/null +++ b/deploy/azure/main.bicep @@ -0,0 +1,139 @@ +@description('FinMind Azure Container Apps deployment') + +param location string = resourceGroup().location +param backendImage string +param frontendImage string + +@secure() +param databaseUrl string +@secure() +param redisUrl string +@secure() +param jwtSecret string + +// Log Analytics Workspace +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: 'finmind-logs' + location: location + properties: { + sku: { name: 'PerGB2018' } + retentionInDays: 30 + } +} + +// Container Apps Environment +resource containerEnv 'Microsoft.App/managedEnvironments@2023-05-01' = { + name: 'finmind-env' + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } +} + +// Backend Container App +resource backend 'Microsoft.App/containerApps@2023-05-01' = { + name: 'finmind-backend' + location: location + properties: { + managedEnvironmentId: containerEnv.id + configuration: { + ingress: { + external: true + targetPort: 8000 + transport: 'http' + } + secrets: [ + { name: 'database-url', value: databaseUrl } + { name: 'redis-url', value: redisUrl } + { name: 'jwt-secret', value: jwtSecret } + ] + } + template: { + containers: [ + { + name: 'backend' + image: backendImage + resources: { + cpu: json('0.25') + memory: '0.5Gi' + } + env: [ + { name: 'DATABASE_URL', secretRef: 'database-url' } + { name: 'REDIS_URL', secretRef: 'redis-url' } + { name: 'JWT_SECRET', secretRef: 'jwt-secret' } + { name: 'LOG_LEVEL', value: 'INFO' } + { name: 'GEMINI_MODEL', value: 'gemini-1.5-flash' } + ] + command: [ + 'sh', '-c', + 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app' + ] + probes: [ + { + type: 'Liveness' + httpGet: { path: '/health', port: 8000 } + periodSeconds: 20 + } + { + type: 'Readiness' + httpGet: { path: '/health', port: 8000 } + initialDelaySeconds: 10 + periodSeconds: 10 + } + ] + } + ] + scale: { + minReplicas: 1 + maxReplicas: 3 + rules: [ + { + name: 'http-scaling' + http: { metadata: { concurrentRequests: '50' } } + } + ] + } + } + } +} + +// Frontend Container App +resource frontend 'Microsoft.App/containerApps@2023-05-01' = { + name: 'finmind-frontend' + location: location + properties: { + managedEnvironmentId: containerEnv.id + configuration: { + ingress: { + external: true + targetPort: 80 + transport: 'http' + } + } + template: { + containers: [ + { + name: 'frontend' + image: frontendImage + resources: { + cpu: json('0.25') + memory: '0.5Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 2 + } + } + } +} + +output backendUrl string = 'https://${backend.properties.configuration.ingress.fqdn}' +output frontendUrl string = 'https://${frontend.properties.configuration.ingress.fqdn}' diff --git a/deploy/droplet/setup.sh b/deploy/droplet/setup.sh new file mode 100755 index 00000000..157c7798 --- /dev/null +++ b/deploy/droplet/setup.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# FinMind — DigitalOcean Droplet one-click setup +# Run as root on a fresh Ubuntu 22.04+ droplet: +# curl -sSL https://raw.githubusercontent.com/rohitdash08/FinMind/main/deploy/droplet/setup.sh | bash +set -euo pipefail + +echo "=== FinMind Droplet Setup ===" + +# Install Docker +if ! command -v docker &>/dev/null; then + echo "Installing Docker..." + curl -fsSL https://get.docker.com | sh + systemctl enable --now docker +fi + +# Install Docker Compose plugin +if ! docker compose version &>/dev/null; then + echo "Installing Docker Compose..." + apt-get update && apt-get install -y docker-compose-plugin +fi + +# Clone repo +INSTALL_DIR="/opt/finmind" +if [ ! -d "$INSTALL_DIR" ]; then + git clone https://github.com/rohitdash08/FinMind.git "$INSTALL_DIR" +else + cd "$INSTALL_DIR" && git pull +fi +cd "$INSTALL_DIR" + +# Create .env from example if not exists +if [ ! -f .env ]; then + cp .env.example .env + # Generate a random JWT secret + JWT=$(openssl rand -hex 32) + sed -i "s/JWT_SECRET=\"change-me\"/JWT_SECRET=\"$JWT\"/" .env + echo "Created .env with random JWT_SECRET. Edit /opt/finmind/.env for other settings." +fi + +# Start services (production profile: backend, frontend via nginx, postgres, redis) +# The docker-compose.yml includes a dev frontend on :5173 and nginx on :8080. +# For production, both are started. Access the app via nginx on port 8080. +docker compose up -d + +PUBLIC_IP=$(curl -s --max-time 5 ifconfig.me || echo "") + +echo "" +echo "=== ✅ FinMind is starting! ===" +echo "" +echo "Frontend: http://${PUBLIC_IP}:8080" +echo "Backend: http://${PUBLIC_IP}:8000/health" +echo "Grafana: http://${PUBLIC_IP}:3000" +echo "" +echo "Edit /opt/finmind/.env and run 'docker compose restart' to update config." diff --git a/deploy/fly/deploy.sh b/deploy/fly/deploy.sh new file mode 100755 index 00000000..948367e0 --- /dev/null +++ b/deploy/fly/deploy.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Deploy FinMind to Fly.io +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "=== FinMind Fly.io Deployment ===" + +# Check fly CLI +if ! command -v fly &>/dev/null; then + echo "Error: flyctl not found." + echo "Install: https://fly.io/docs/hands-on/install-flyctl/" + exit 1 +fi + +# Check authentication +if ! fly auth whoami &>/dev/null 2>&1; then + echo "Not logged in. Running 'fly auth login'..." + fly auth login +fi + +echo "" +echo "Step 1: Creating Postgres cluster..." +fly postgres create --name finmind-db --region iad \ + --vm-size shared-cpu-1x --initial-cluster-size 1 --volume-size 1 \ + 2>/dev/null || echo " (Postgres cluster 'finmind-db' may already exist, continuing)" + +echo "" +echo "Step 2: Creating Redis (via Upstash)..." +fly redis create --name finmind-redis --region iad --no-replicas \ + 2>/dev/null || echo " (Redis 'finmind-redis' may already exist, continuing)" + +echo "" +echo "Step 3: Deploying backend..." +cd "$REPO_ROOT/packages/backend" +fly deploy --config "$SCRIPT_DIR/fly.backend.toml" --remote-only + +echo "" +echo "Step 4: Attaching Postgres to backend..." +fly postgres attach finmind-db --app finmind-backend \ + 2>/dev/null || echo " (Already attached)" + +echo "" +echo "Step 5: Setting secrets on backend..." +# Try to get Redis URL from Upstash +REDIS_URL=$(fly redis status finmind-redis --json 2>/dev/null | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") +if [ -n "$REDIS_URL" ]; then + fly secrets set --app finmind-backend \ + JWT_SECRET="$(openssl rand -hex 32)" \ + REDIS_URL="$REDIS_URL" + echo " Redis URL set automatically from Upstash." +else + fly secrets set --app finmind-backend \ + JWT_SECRET="$(openssl rand -hex 32)" + echo "" + echo " ⚠️ Could not auto-detect Redis URL." + echo " Set it manually: fly secrets set --app finmind-backend REDIS_URL=" + echo " (Check 'fly redis status finmind-redis' for the connection string)" +fi + +echo "" +echo "Step 6: Deploying frontend..." +cd "$REPO_ROOT/app" +fly deploy --config "$SCRIPT_DIR/fly.frontend.toml" --remote-only + +echo "" +echo "=== ✅ Deployment complete ===" +echo "" +echo "Backend: https://finmind-backend.fly.dev/health" +echo "Frontend: https://finmind-frontend.fly.dev" +echo "" +echo "Next steps:" +echo " 1. Set VITE_API_URL on the frontend if needed" +echo " 2. Verify: curl https://finmind-backend.fly.dev/health" +echo " 3. Open https://finmind-frontend.fly.dev in your browser" diff --git a/deploy/fly/fly.backend.toml b/deploy/fly/fly.backend.toml new file mode 100644 index 00000000..152e2837 --- /dev/null +++ b/deploy/fly/fly.backend.toml @@ -0,0 +1,38 @@ +app = "finmind-backend" +primary_region = "iad" +kill_signal = "SIGINT" +kill_timeout = "5s" + +[build] + dockerfile = "Dockerfile" + +[deploy] + release_command = "python -m flask --app wsgi:app init-db" + +[env] + LOG_LEVEL = "INFO" + GEMINI_MODEL = "gemini-1.5-flash" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 1 + + [http_service.concurrency] + type = "requests" + hard_limit = 250 + soft_limit = 200 + +[[http_service.checks]] + interval = "10s" + timeout = "5s" + grace_period = "20s" + method = "GET" + path = "/health" + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 512 diff --git a/deploy/fly/fly.frontend.toml b/deploy/fly/fly.frontend.toml new file mode 100644 index 00000000..bc42a891 --- /dev/null +++ b/deploy/fly/fly.frontend.toml @@ -0,0 +1,22 @@ +app = "finmind-frontend" +primary_region = "iad" +kill_signal = "SIGINT" +kill_timeout = "5s" + +[build] + dockerfile = "Dockerfile" + +[env] + BACKEND_URL = "https://finmind-backend.fly.dev" + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 1 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 diff --git a/deploy/gcp/cloudbuild.yaml b/deploy/gcp/cloudbuild.yaml new file mode 100644 index 00000000..ffd6691c --- /dev/null +++ b/deploy/gcp/cloudbuild.yaml @@ -0,0 +1,61 @@ +steps: + # Build backend image + - name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', 'gcr.io/$PROJECT_ID/finmind-backend:$COMMIT_SHA', '-f', 'packages/backend/Dockerfile', 'packages/backend'] + + # Build frontend image + - name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', 'gcr.io/$PROJECT_ID/finmind-frontend:$COMMIT_SHA', '-f', 'app/Dockerfile', 'app'] + + # Push images + - name: 'gcr.io/cloud-builders/docker' + args: ['push', 'gcr.io/$PROJECT_ID/finmind-backend:$COMMIT_SHA'] + + - name: 'gcr.io/cloud-builders/docker' + args: ['push', 'gcr.io/$PROJECT_ID/finmind-frontend:$COMMIT_SHA'] + + # Deploy backend to Cloud Run + - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' + entrypoint: gcloud + args: + - 'run' + - 'deploy' + - 'finmind-backend' + - '--image=gcr.io/$PROJECT_ID/finmind-backend:$COMMIT_SHA' + - '--region=$_REGION' + - '--platform=managed' + - '--port=8000' + - '--allow-unauthenticated' + - '--set-env-vars=LOG_LEVEL=INFO,GEMINI_MODEL=gemini-1.5-flash' + - '--update-secrets=DATABASE_URL=finmind-database-url:latest,REDIS_URL=finmind-redis-url:latest,JWT_SECRET=finmind-jwt-secret:latest' + - '--min-instances=0' + - '--max-instances=3' + - '--cpu=1' + - '--memory=512Mi' + + # Deploy frontend to Cloud Run + - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' + entrypoint: gcloud + args: + - 'run' + - 'deploy' + - 'finmind-frontend' + - '--image=gcr.io/$PROJECT_ID/finmind-frontend:$COMMIT_SHA' + - '--region=$_REGION' + - '--platform=managed' + - '--port=80' + - '--allow-unauthenticated' + - '--min-instances=0' + - '--max-instances=2' + - '--cpu=1' + - '--memory=256Mi' + +substitutions: + _REGION: us-east1 + +images: + - 'gcr.io/$PROJECT_ID/finmind-backend:$COMMIT_SHA' + - 'gcr.io/$PROJECT_ID/finmind-frontend:$COMMIT_SHA' + +options: + logging: CLOUD_LOGGING_ONLY diff --git a/deploy/gcp/cloudrun-backend.yaml b/deploy/gcp/cloudrun-backend.yaml new file mode 100644 index 00000000..706d2960 --- /dev/null +++ b/deploy/gcp/cloudrun-backend.yaml @@ -0,0 +1,56 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: finmind-backend + annotations: + run.googleapis.com/launch-stage: GA +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/minScale: "0" + autoscaling.knative.dev/maxScale: "3" + run.googleapis.com/cpu-throttling: "true" + spec: + containerConcurrency: 80 + timeoutSeconds: 300 + containers: + - image: gcr.io/PROJECT_ID/finmind-backend:latest + ports: + - containerPort: 8000 + resources: + limits: + cpu: "1" + memory: 512Mi + env: + - name: LOG_LEVEL + value: INFO + - name: GEMINI_MODEL + value: gemini-1.5-flash + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: finmind-database-url + key: latest + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: finmind-redis-url + key: latest + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: finmind-jwt-secret + key: latest + startupProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: 8000 + periodSeconds: 20 diff --git a/deploy/helm/finmind/Chart.yaml b/deploy/helm/finmind/Chart.yaml new file mode 100644 index 00000000..16a34107 --- /dev/null +++ b/deploy/helm/finmind/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: finmind +description: FinMind - AI-powered personal finance manager +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - finance + - budgeting + - flask + - react +maintainers: + - name: FinMind Team diff --git a/deploy/helm/finmind/templates/_helpers.tpl b/deploy/helm/finmind/templates/_helpers.tpl new file mode 100644 index 00000000..4488c6f7 --- /dev/null +++ b/deploy/helm/finmind/templates/_helpers.tpl @@ -0,0 +1,10 @@ +{{- define "finmind.fullname" -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "finmind.labels" -}} +app.kubernetes.io/name: finmind +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +{{- end -}} diff --git a/deploy/helm/finmind/templates/backend.yaml b/deploy/helm/finmind/templates/backend.yaml new file mode 100644 index 00000000..436dc0fd --- /dev/null +++ b/deploy/helm/finmind/templates/backend.yaml @@ -0,0 +1,105 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-backend + labels: + {{- include "finmind.labels" . | nindent 4 }} + app: {{ include "finmind.fullname" . }}-backend + app.kubernetes.io/component: backend +spec: + replicas: {{ .Values.backend.replicas }} + selector: + matchLabels: + app: {{ include "finmind.fullname" . }}-backend + template: + metadata: + labels: + app: {{ include "finmind.fullname" . }}-backend + spec: + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: POSTGRES_DB + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: JWT_SECRET + - name: GEMINI_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: GEMINI_API_KEY + - name: DATABASE_URL + value: "postgresql+psycopg2://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@{{ include "finmind.fullname" . }}-postgres:5432/$(POSTGRES_DB)" + - name: REDIS_URL + valueFrom: + configMapKeyRef: + name: {{ include "finmind.fullname" . }}-config + key: REDIS_URL + - name: LOG_LEVEL + valueFrom: + configMapKeyRef: + name: {{ include "finmind.fullname" . }}-config + key: LOG_LEVEL + - name: GEMINI_MODEL + valueFrom: + configMapKeyRef: + name: {{ include "finmind.fullname" . }}-config + key: GEMINI_MODEL + command: + - sh + - -c + - | + python -m flask --app wsgi:app init-db && \ + export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc && \ + rm -rf $PROMETHEUS_MULTIPROC_DIR && \ + mkdir -p $PROMETHEUS_MULTIPROC_DIR && \ + gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 20 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-backend + labels: + {{- include "finmind.labels" . | nindent 4 }} + app: {{ include "finmind.fullname" . }}-backend +spec: + selector: + app: {{ include "finmind.fullname" . }}-backend + ports: + - name: http + port: 8000 + targetPort: 8000 diff --git a/deploy/helm/finmind/templates/configmap.yaml b/deploy/helm/finmind/templates/configmap.yaml new file mode 100644 index 00000000..8c251b3d --- /dev/null +++ b/deploy/helm/finmind/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "finmind.fullname" . }}-config + labels: + {{- include "finmind.labels" . | nindent 4 }} +data: + LOG_LEVEL: {{ .Values.backend.env.LOG_LEVEL | quote }} + GEMINI_MODEL: {{ .Values.backend.env.GEMINI_MODEL | quote }} + REDIS_URL: {{ .Values.backend.env.REDIS_URL | quote }} diff --git a/deploy/helm/finmind/templates/frontend.yaml b/deploy/helm/finmind/templates/frontend.yaml new file mode 100644 index 00000000..022d2ae7 --- /dev/null +++ b/deploy/helm/finmind/templates/frontend.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-frontend + labels: + {{- include "finmind.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + replicas: {{ .Values.frontend.replicas }} + selector: + matchLabels: + app: {{ include "finmind.fullname" . }}-frontend + template: + metadata: + labels: + app: {{ include "finmind.fullname" . }}-frontend + spec: + containers: + - name: frontend + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + ports: + - containerPort: 80 + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 20 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-frontend + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + selector: + app: {{ include "finmind.fullname" . }}-frontend + ports: + - port: 80 + targetPort: 80 diff --git a/deploy/helm/finmind/templates/hpa.yaml b/deploy/helm/finmind/templates/hpa.yaml new file mode 100644 index 00000000..e865a24f --- /dev/null +++ b/deploy/helm/finmind/templates/hpa.yaml @@ -0,0 +1,22 @@ +{{- if .Values.backend.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "finmind.fullname" . }}-backend + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "finmind.fullname" . }}-backend + minReplicas: {{ .Values.backend.hpa.minReplicas }} + maxReplicas: {{ .Values.backend.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.backend.hpa.targetCPUUtilization }} +{{- end }} diff --git a/deploy/helm/finmind/templates/ingress.yaml b/deploy/helm/finmind/templates/ingress.yaml new file mode 100644 index 00000000..e45d529a --- /dev/null +++ b/deploy/helm/finmind/templates/ingress.yaml @@ -0,0 +1,37 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "finmind.fullname" . }}-ingress + labels: + {{- include "finmind.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - secretName: {{ .secretName }} + hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "finmind.fullname" $ }}-{{ .service }} + port: + number: {{ if eq .service "backend" }}8000{{ else }}80{{ end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/postgres.yaml b/deploy/helm/finmind/templates/postgres.yaml new file mode 100644 index 00000000..e34a60bc --- /dev/null +++ b/deploy/helm/finmind/templates/postgres.yaml @@ -0,0 +1,69 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "finmind.fullname" . }}-postgres-data + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: {{ .Values.postgres.storage }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-postgres + labels: + {{- include "finmind.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "finmind.fullname" . }}-postgres + template: + metadata: + labels: + app: {{ include "finmind.fullname" . }}-postgres + spec: + containers: + - name: postgres + image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + ports: + - containerPort: 5432 + envFrom: + - secretRef: + name: {{ include "finmind.fullname" . }}-secrets + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\""] + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\""] + initialDelaySeconds: 15 + periodSeconds: 20 + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "finmind.fullname" . }}-postgres-data +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-postgres + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + selector: + app: {{ include "finmind.fullname" . }}-postgres + ports: + - port: 5432 + targetPort: 5432 +{{- end }} diff --git a/deploy/helm/finmind/templates/redis.yaml b/deploy/helm/finmind/templates/redis.yaml new file mode 100644 index 00000000..dbd483e9 --- /dev/null +++ b/deploy/helm/finmind/templates/redis.yaml @@ -0,0 +1,45 @@ +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-redis + labels: + {{- include "finmind.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "finmind.fullname" . }}-redis + template: + metadata: + labels: + app: {{ include "finmind.fullname" . }}-redis + spec: + containers: + - name: redis + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" + ports: + - containerPort: 6379 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + periodSeconds: 10 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + periodSeconds: 20 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-redis + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + selector: + app: {{ include "finmind.fullname" . }}-redis + ports: + - port: 6379 + targetPort: 6379 +{{- end }} diff --git a/deploy/helm/finmind/templates/secrets.yaml b/deploy/helm/finmind/templates/secrets.yaml new file mode 100644 index 00000000..63fc81d8 --- /dev/null +++ b/deploy/helm/finmind/templates/secrets.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "finmind.fullname" . }}-secrets + labels: + {{- include "finmind.labels" . | nindent 4 }} + annotations: + # For use with external-secrets or sealed-secrets: + # sealedsecrets.bitnami.com/managed: "true" + # externalsecrets.kubernetes-client.io/managed: "true" +type: Opaque +stringData: + POSTGRES_USER: {{ .Values.postgres.env.POSTGRES_USER | quote }} + POSTGRES_PASSWORD: {{ required "secrets.postgresPassword is required" .Values.secrets.postgresPassword | quote }} + POSTGRES_DB: {{ .Values.postgres.env.POSTGRES_DB | quote }} + JWT_SECRET: {{ required "secrets.jwtSecret is required" .Values.secrets.jwtSecret | quote }} + GEMINI_API_KEY: {{ .Values.secrets.geminiApiKey | default "" | quote }} diff --git a/deploy/helm/finmind/templates/servicemonitor.yaml b/deploy/helm/finmind/templates/servicemonitor.yaml new file mode 100644 index 00000000..033f4730 --- /dev/null +++ b/deploy/helm/finmind/templates/servicemonitor.yaml @@ -0,0 +1,16 @@ +{{- if .Values.monitoring.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "finmind.fullname" . }}-backend + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: {{ include "finmind.fullname" . }}-backend + endpoints: + - port: http + path: /metrics + interval: {{ .Values.monitoring.serviceMonitor.interval }} +{{- end }} diff --git a/deploy/helm/finmind/values.yaml b/deploy/helm/finmind/values.yaml new file mode 100644 index 00000000..d1f76488 --- /dev/null +++ b/deploy/helm/finmind/values.yaml @@ -0,0 +1,100 @@ +backend: + image: + repository: ghcr.io/rohitdash08/finmind-backend + tag: latest + pullPolicy: IfNotPresent + replicas: 2 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + hpa: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilization: 70 + env: + LOG_LEVEL: INFO + GEMINI_MODEL: gemini-1.5-flash + REDIS_URL: redis://finmind-redis:6379/0 + +frontend: + image: + repository: ghcr.io/rohitdash08/finmind-frontend + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +postgres: + enabled: true + image: + repository: postgres + tag: "16" + storage: 10Gi + env: + POSTGRES_USER: finmind + POSTGRES_DB: finmind + +redis: + enabled: true + image: + repository: redis + tag: "7" + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: finmind.example.com + paths: + - path: /health + pathType: Prefix + service: backend + - path: /auth + pathType: Prefix + service: backend + - path: /expenses + pathType: Prefix + service: backend + - path: /bills + pathType: Prefix + service: backend + - path: /reminders + pathType: Prefix + service: backend + - path: /dashboard + pathType: Prefix + service: backend + - path: /insights + pathType: Prefix + service: backend + - path: / + pathType: Prefix + service: frontend + tls: + - secretName: finmind-tls + hosts: + - finmind.example.com + +secrets: + # Set these via --set or a separate values file + jwtSecret: "" + postgresPassword: "" + geminiApiKey: "" + +monitoring: + serviceMonitor: + enabled: false + interval: 30s diff --git a/deploy/heroku/Procfile b/deploy/heroku/Procfile new file mode 100644 index 00000000..dde34cc5 --- /dev/null +++ b/deploy/heroku/Procfile @@ -0,0 +1 @@ +web: python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:$PORT wsgi:app diff --git a/deploy/heroku/app.json b/deploy/heroku/app.json new file mode 100644 index 00000000..c129152a --- /dev/null +++ b/deploy/heroku/app.json @@ -0,0 +1,49 @@ +{ + "name": "FinMind", + "description": "AI-powered personal finance manager", + "repository": "https://github.com/rohitdash08/FinMind", + "logo": "", + "keywords": ["finance", "budgeting", "flask", "react"], + "stack": "container", + "addons": [ + { + "plan": "heroku-postgresql:essential-0" + }, + { + "plan": "heroku-redis:mini" + } + ], + "env": { + "JWT_SECRET": { + "description": "Secret key for JWT token signing", + "generator": "secret" + }, + "DATABASE_URL": { + "description": "PostgreSQL connection URL (auto-set by addon)" + }, + "REDIS_URL": { + "description": "Redis connection URL (auto-set by addon)" + }, + "LOG_LEVEL": { + "description": "Logging level", + "value": "INFO" + }, + "GEMINI_API_KEY": { + "description": "Google Gemini API key for AI features", + "required": false + }, + "GEMINI_MODEL": { + "description": "Gemini model name", + "value": "gemini-1.5-flash" + } + }, + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + }, + "buildpacks": [], + "scripts": {}, + "environments": {} +} diff --git a/deploy/heroku/heroku.yml b/deploy/heroku/heroku.yml new file mode 100644 index 00000000..40df6e02 --- /dev/null +++ b/deploy/heroku/heroku.yml @@ -0,0 +1,10 @@ +build: + docker: + web: packages/backend/Dockerfile +run: + web: + command: + - sh + - -c + - "python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:$PORT wsgi:app" + image: web diff --git a/deploy/railway/README.md b/deploy/railway/README.md new file mode 100644 index 00000000..53a1832c --- /dev/null +++ b/deploy/railway/README.md @@ -0,0 +1,43 @@ +# Railway Deployment + +## Quick Start + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/new?template=https://github.com/rohitdash08/FinMind) + +## Manual Setup + +1. Install the [Railway CLI](https://docs.railway.app/develop/cli) or use the dashboard. + +2. Create a new project: + ```bash + railway login + railway init + ``` + +3. Add services in the Railway dashboard: + - **PostgreSQL** — Add from the database menu + - **Redis** — Add from the database menu + - **Backend** — Link this repo, set root directory to `/packages/backend` + - **Frontend** — Link this repo, set root directory to `/app` + +4. Set environment variables on the backend service: + - `DATABASE_URL` — Reference from Railway Postgres (auto-provided) + - `REDIS_URL` — Reference from Railway Redis (auto-provided) + - `JWT_SECRET` — Generate: `openssl rand -hex 32` + - `LOG_LEVEL` — `INFO` + +5. Set environment variables on the frontend service: + - `VITE_API_URL` — Set to the backend service's public URL + +6. Deploy: + ```bash + railway up + ``` + +## Notes + +- Railway auto-detects Dockerfiles in each service root. +- The `railway.json` in this directory configures the backend service. +- For the frontend, Railway will detect the Dockerfile in `app/` and serve on port 80. +- Railway provides `DATABASE_URL` as `postgres://...` — the backend auto-converts to `postgresql+psycopg2://`. +- Railway assigns a dynamic `PORT` — the start command binds to `${PORT:-8000}`. diff --git a/deploy/railway/railway.json b/deploy/railway/railway.json new file mode 100644 index 00000000..e35936b1 --- /dev/null +++ b/deploy/railway/railway.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "packages/backend/Dockerfile" + }, + "deploy": { + "numReplicas": 1, + "startCommand": "sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:${PORT:-8000} wsgi:app'", + "healthcheckPath": "/health", + "healthcheckTimeout": 30, + "restartPolicyType": "ON_FAILURE" + } +} diff --git a/docs/deployment/README.md b/docs/deployment/README.md new file mode 100644 index 00000000..c7a56d37 --- /dev/null +++ b/docs/deployment/README.md @@ -0,0 +1,190 @@ +# FinMind Deployment Guide + +FinMind supports one-click deployment to multiple platforms. Choose the one that fits your needs. + +## Quick Start + +Run the interactive deploy script: + +```bash +bash scripts/deploy.sh +``` + +## Architecture + +- **Frontend**: Vite/React app built to static files, served by nginx on port 80 +- **Backend**: Flask API served by gunicorn on port 8000 +- **Database**: PostgreSQL 16 +- **Cache**: Redis 7 +- **Monitoring**: Prometheus + Grafana + Loki (optional) + +### Required Environment Variables + +| Variable | Description | +|----------|-------------| +| `DATABASE_URL` | PostgreSQL connection string (`postgresql+psycopg2://...` or `postgres://...` — auto-converted) | +| `REDIS_URL` | Redis connection string | +| `JWT_SECRET` | Secret for JWT token signing | +| `VITE_API_URL` | Backend URL (frontend build-time only) | + +See `.env.example` for all variables. + +--- + +## Platform Guides + +### Docker Compose (Local/VPS) + +```bash +cp .env.example .env +# Edit .env with your values +docker compose up -d +``` + +- Frontend: http://localhost:5173 (dev) or build with `app/Dockerfile` +- Backend: http://localhost:8000/health +- Grafana: http://localhost:3000 + +### Kubernetes + +**Raw manifests:** +```bash +bash scripts/deploy-k8s.sh +``` + +**Helm chart** (recommended): +```bash +helm upgrade --install finmind deploy/helm/finmind \ + --namespace finmind --create-namespace \ + --set secrets.jwtSecret="$(openssl rand -hex 32)" \ + --set secrets.postgresPassword="$(openssl rand -hex 16)" +``` + +Features: HPA, Ingress with TLS (cert-manager), ServiceMonitor, health probes. + +See: [Kubernetes Guide](./kubernetes.md) + +### Railway + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/new?template=https://github.com/rohitdash08/FinMind) + +1. Install [Railway CLI](https://docs.railway.app/develop/cli) +2. `railway login && railway init` +3. Add PostgreSQL and Redis plugins in the dashboard +4. Set env vars and deploy: `railway up` + +See: [Railway Guide](./railway.md) + +### Heroku + +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/rohitdash08/FinMind) + +Or manually: +```bash +heroku create finmind-app +heroku stack:set container +heroku addons:create heroku-postgresql:essential-0 +heroku addons:create heroku-redis:mini +git push heroku main +``` + +See: [Heroku Guide](./heroku.md) + +### DigitalOcean + +**App Platform:** +```bash +doctl apps create --spec .do/app.yaml +``` + +**Droplet:** +```bash +curl -sSL https://raw.githubusercontent.com/rohitdash08/FinMind/main/deploy/droplet/setup.sh | bash +``` + +See: [DigitalOcean Guide](./digitalocean.md) + +### Render + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/rohitdash08/FinMind) + +1. Push to GitHub +2. Go to [Render Blueprints](https://dashboard.render.com/blueprints) +3. Connect repo — Render auto-detects `render.yaml` + +See: [Render Guide](./render.md) + +### Fly.io + +```bash +bash deploy/fly/deploy.sh +``` + +See: [Fly.io Guide](./flyio.md) + +### AWS + +**ECS Fargate (CloudFormation):** +```bash +aws cloudformation deploy --template-file deploy/aws/cloudformation.yaml \ + --stack-name finmind --capabilities CAPABILITY_NAMED_IAM \ + --parameter-overrides VpcId= SubnetIds= ... +``` + +**App Runner:** See `deploy/aws/apprunner.yaml` + +See: [AWS Guide](./aws.md) + +### GCP Cloud Run + +```bash +gcloud builds submit --config deploy/gcp/cloudbuild.yaml +``` + +See: [GCP Guide](./gcp.md) + +### Azure Container Apps + +```bash +az deployment group create --resource-group finmind-rg \ + --template-file deploy/azure/main.bicep \ + --parameters backendImage= frontendImage= ... +``` + +See: [Azure Guide](./azure.md) + +### Netlify (Frontend Only) + +Connect GitHub repo at [Netlify](https://app.netlify.com). Auto-detects `netlify.toml`. +Set `VITE_API_URL` to your backend URL in site environment variables. + +### Vercel (Frontend Only) + +```bash +cd app && vercel --prod +``` + +Set `VITE_API_URL` environment variable in the Vercel dashboard. + +--- + +## Verification Checklist + +After deploying on any platform, verify: + +- [ ] Frontend loads and is reachable +- [ ] `GET /health` returns 200 on the backend +- [ ] Database is connected (user registration works) +- [ ] Redis is connected (sessions/caching work) +- [ ] Auth flows: register, login, token refresh +- [ ] Core modules: expenses, bills, reminders, dashboard, insights + +### Local K8s Development + +Use [Tilt](https://tilt.dev) for live-reload K8s dev: + +```bash +tilt up +``` + +This builds images, applies manifests, sets up port forwarding, and live-reloads on code changes. diff --git a/docs/deployment/assets/demo.gif b/docs/deployment/assets/demo.gif new file mode 100644 index 00000000..056b5946 Binary files /dev/null and b/docs/deployment/assets/demo.gif differ diff --git a/docs/deployment/aws.md b/docs/deployment/aws.md new file mode 100644 index 00000000..e7521ad2 --- /dev/null +++ b/docs/deployment/aws.md @@ -0,0 +1,71 @@ +# AWS Deployment Guide + +## Option A: ECS Fargate (CloudFormation) + +### Prerequisites +- AWS CLI configured with credentials +- A VPC with public subnets +- ECR repositories for backend and frontend images +- RDS PostgreSQL instance +- ElastiCache Redis instance + +### Steps + +1. **Build and push Docker images to ECR:** + ```bash + # Create ECR repos + aws ecr create-repository --repository-name finmind-backend + aws ecr create-repository --repository-name finmind-frontend + + # Login to ECR + aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com + + # Build and push + docker build -t finmind-backend -f packages/backend/Dockerfile packages/backend + docker tag finmind-backend:latest .dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest + docker push .dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest + + docker build -t finmind-frontend -f app/Dockerfile app + docker tag finmind-frontend:latest .dkr.ecr.us-east-1.amazonaws.com/finmind-frontend:latest + docker push .dkr.ecr.us-east-1.amazonaws.com/finmind-frontend:latest + ``` + +2. **Deploy the CloudFormation stack:** + ```bash + aws cloudformation deploy \ + --template-file deploy/aws/cloudformation.yaml \ + --stack-name finmind \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameter-overrides \ + VpcId=vpc-xxxxx \ + SubnetIds=subnet-aaa,subnet-bbb \ + BackendImage=.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest \ + FrontendImage=.dkr.ecr.us-east-1.amazonaws.com/finmind-frontend:latest \ + DatabaseUrl=postgresql+psycopg2://user:pass@rds-host:5432/finmind \ + RedisUrl=redis://elasticache-host:6379/0 \ + JwtSecret=$(openssl rand -hex 32) + ``` + +3. **Verify:** + ```bash + aws ecs list-services --cluster finmind + # Get task public IP and check /health + ``` + +## Option B: App Runner + +Simpler but less configurable. See `deploy/aws/apprunner.yaml` for the service config reference. Convert it to JSON for the CLI, or use the AWS Console. + +```bash +yq -o=json deploy/aws/apprunner.yaml > /tmp/apprunner.json +aws apprunner create-service --cli-input-json file:///tmp/apprunner.json +``` + +**Note:** App Runner doesn't support multi-container. Deploy backend only; host frontend on S3+CloudFront or Amplify. + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/azure.md b/docs/deployment/azure.md new file mode 100644 index 00000000..ec06a910 --- /dev/null +++ b/docs/deployment/azure.md @@ -0,0 +1,48 @@ +# Azure Container Apps Deployment Guide + +## Prerequisites +- Azure CLI installed and logged in (`az login`) +- A resource group (`az group create --name finmind-rg --location eastus`) +- Azure Container Registry (ACR) with built images +- Azure Database for PostgreSQL flexible server +- Azure Cache for Redis + +## Steps + +1. **Create ACR and push images:** + ```bash + az acr create --resource-group finmind-rg --name finmindacr --sku Basic + az acr login --name finmindacr + + docker build -t finmindacr.azurecr.io/finmind-backend -f packages/backend/Dockerfile packages/backend + docker push finmindacr.azurecr.io/finmind-backend + + docker build -t finmindacr.azurecr.io/finmind-frontend -f app/Dockerfile app + docker push finmindacr.azurecr.io/finmind-frontend + ``` + +2. **Deploy with Bicep:** + ```bash + az deployment group create \ + --resource-group finmind-rg \ + --template-file deploy/azure/main.bicep \ + --parameters \ + backendImage=finmindacr.azurecr.io/finmind-backend:latest \ + frontendImage=finmindacr.azurecr.io/finmind-frontend:latest \ + databaseUrl='postgresql+psycopg2://user:pass@pg-host:5432/finmind' \ + redisUrl='redis://redis-host:6380?ssl=true' \ + jwtSecret=$(openssl rand -hex 32) + ``` + +3. **Get endpoints:** + ```bash + az containerapp show --name finmind-backend --resource-group finmind-rg --query properties.configuration.ingress.fqdn + az containerapp show --name finmind-frontend --resource-group finmind-rg --query properties.configuration.ingress.fqdn + ``` + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 on backend FQDN +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/digitalocean.md b/docs/deployment/digitalocean.md new file mode 100644 index 00000000..11568fa4 --- /dev/null +++ b/docs/deployment/digitalocean.md @@ -0,0 +1,59 @@ +# DigitalOcean Deployment Guide + +## Option A: App Platform + +### Prerequisites +- `doctl` CLI installed and authenticated +- GitHub repo connected to DigitalOcean + +### Steps + +1. **Deploy from spec:** + ```bash + doctl apps create --spec .do/app.yaml + ``` + Or connect the repo via the [DigitalOcean dashboard](https://cloud.digitalocean.com/apps) — it auto-detects `.do/app.yaml`. + +2. **Set environment variables** in the dashboard: + - `JWT_SECRET` — generate with `openssl rand -hex 32` + - `GEMINI_API_KEY` — optional, for AI features + - `VITE_API_URL` — set on the frontend static site to the backend service URL + +3. **Note on Redis:** DO App Platform doesn't have managed Redis in app specs. Options: + - Use a [DigitalOcean Managed Redis](https://cloud.digitalocean.com/databases) cluster and set `REDIS_URL` on the backend service + - Use [Upstash Redis](https://upstash.com) (free tier available) + +4. **Note on DATABASE_URL:** DigitalOcean provides `postgres://` connection strings. + The backend automatically converts these to the `postgresql+psycopg2://` format + required by SQLAlchemy. + +## Option B: Droplet + +### Prerequisites +- A fresh Ubuntu 22.04+ droplet (1GB+ RAM recommended) +- SSH access as root + +### Steps + +1. **SSH into your droplet and run:** + ```bash + curl -sSL https://raw.githubusercontent.com/rohitdash08/FinMind/main/deploy/droplet/setup.sh | bash + ``` + +2. **Edit configuration:** + ```bash + nano /opt/finmind/.env + docker compose -f /opt/finmind/docker-compose.yml restart + ``` + +3. **Endpoints:** + - Frontend: `http://:8080` (via nginx) + - Backend: `http://:8000/health` + - Grafana: `http://:3000` + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/flyio.md b/docs/deployment/flyio.md new file mode 100644 index 00000000..ad62774b --- /dev/null +++ b/docs/deployment/flyio.md @@ -0,0 +1,55 @@ +# Fly.io Deployment Guide + +## Prerequisites +- [flyctl](https://fly.io/docs/hands-on/install-flyctl/) installed +- Fly.io account (`fly auth login`) + +## Quick Deploy + +Run the automated deploy script: +```bash +bash deploy/fly/deploy.sh +``` + +This will: +1. Create a Fly Postgres cluster (`finmind-db`) +2. Create a Fly Redis instance via Upstash (`finmind-redis`) +3. Deploy the backend from `packages/backend/` using `deploy/fly/fly.backend.toml` +4. Attach Postgres and set secrets +5. Deploy the frontend from `app/` using `deploy/fly/fly.frontend.toml` + +## Manual Deploy + +```bash +# Create Postgres +fly postgres create --name finmind-db --region iad + +# Create Redis (via Upstash integration) +fly ext redis create --name finmind-redis --region iad + +# Deploy backend (build context = packages/backend/) +fly deploy packages/backend --config deploy/fly/fly.backend.toml --remote-only +fly postgres attach finmind-db --app finmind-backend +fly secrets set --app finmind-backend JWT_SECRET=$(openssl rand -hex 32) +# Set REDIS_URL from the Upstash Redis dashboard or fly ext redis status + +# Deploy frontend (build context = app/) +fly deploy app --config deploy/fly/fly.frontend.toml --remote-only +``` + +## Endpoints +- Backend: `https://finmind-backend.fly.dev/health` +- Frontend: `https://finmind-frontend.fly.dev` + +## Scaling +```bash +fly scale count 2 --app finmind-backend # add replicas +fly scale vm shared-cpu-2x --app finmind-backend # bigger VM +``` + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/gcp.md b/docs/deployment/gcp.md new file mode 100644 index 00000000..9140031c --- /dev/null +++ b/docs/deployment/gcp.md @@ -0,0 +1,60 @@ +# GCP Cloud Run Deployment Guide + +## Prerequisites +- `gcloud` CLI installed and configured +- GCP project with billing enabled +- Cloud SQL PostgreSQL instance +- Memorystore Redis instance (or Upstash) + +## Steps + +1. **Create secrets in Secret Manager:** + ```bash + echo -n "postgresql+psycopg2://user:pass@/finmind?host=/cloudsql/PROJECT:REGION:INSTANCE" | \ + gcloud secrets create finmind-database-url --data-file=- + + echo -n "redis://redis-host:6379/0" | \ + gcloud secrets create finmind-redis-url --data-file=- + + echo -n "$(openssl rand -hex 32)" | \ + gcloud secrets create finmind-jwt-secret --data-file=- + ``` + +2. **Deploy via Cloud Build:** + ```bash + gcloud builds submit --config deploy/gcp/cloudbuild.yaml + ``` + This builds both images, pushes to GCR, and deploys to Cloud Run. + +3. **Or deploy manually:** + ```bash + # Build and push + gcloud builds submit --tag gcr.io/$PROJECT_ID/finmind-backend packages/backend + gcloud builds submit --tag gcr.io/$PROJECT_ID/finmind-frontend app + + # Deploy backend + gcloud run deploy finmind-backend \ + --image gcr.io/$PROJECT_ID/finmind-backend \ + --platform managed --region us-east1 --port 8000 \ + --allow-unauthenticated \ + --update-secrets=DATABASE_URL=finmind-database-url:latest,REDIS_URL=finmind-redis-url:latest,JWT_SECRET=finmind-jwt-secret:latest + + # Deploy frontend + gcloud run deploy finmind-frontend \ + --image gcr.io/$PROJECT_ID/finmind-frontend \ + --platform managed --region us-east1 --port 80 \ + --allow-unauthenticated + ``` + +## Endpoints +```bash +gcloud run services describe finmind-backend --region us-east1 --format 'value(status.url)' +gcloud run services describe finmind-frontend --region us-east1 --format 'value(status.url)' +``` + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md new file mode 100644 index 00000000..38c5e85e --- /dev/null +++ b/docs/deployment/heroku.md @@ -0,0 +1,41 @@ +# Heroku Deployment Guide + +## One-Click Deploy + +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/rohitdash08/FinMind) + +This uses `app.json` at the repo root to provision PostgreSQL, Redis, and set environment variables automatically. + +## Manual CLI Deploy + +```bash +# Create app +heroku create finmind-app +heroku stack:set container -a finmind-app + +# Add add-ons (sets DATABASE_URL and REDIS_URL automatically) +heroku addons:create heroku-postgresql:essential-0 -a finmind-app +heroku addons:create heroku-redis:mini -a finmind-app + +# Set secrets +heroku config:set JWT_SECRET=$(openssl rand -hex 32) -a finmind-app +heroku config:set LOG_LEVEL=INFO GEMINI_MODEL=gemini-1.5-flash -a finmind-app + +# Deploy (uses heroku.yml at repo root) +git push heroku main +``` + +## Notes + +- `heroku.yml` and `app.json` must be at the repo root for Heroku to detect them. +- The backend runs via the container stack using `packages/backend/Dockerfile`. +- Frontend should be deployed separately to Netlify/Vercel (Heroku is backend-only in this setup). +- Heroku auto-sets `DATABASE_URL` (as `postgres://...`) and `REDIS_URL` from add-ons. + The backend automatically converts `postgres://` to `postgresql+psycopg2://` for SQLAlchemy. +- The `$PORT` variable is set by Heroku — gunicorn binds to it automatically. + +## Verification +1. `curl https://finmind-app.herokuapp.com/health` — returns 200 +2. Register a user (DB connected) +3. Check Redis connectivity (sessions work) +4. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/kubernetes.md b/docs/deployment/kubernetes.md new file mode 100644 index 00000000..052c45e7 --- /dev/null +++ b/docs/deployment/kubernetes.md @@ -0,0 +1,54 @@ +# Kubernetes Deployment Guide + +## Prerequisites +- Kubernetes cluster (1.24+) +- `kubectl` configured +- `helm` 3.x (for Helm deployment) +- Container registry with built images + +## Option 1: Raw Manifests +```bash +# Create namespace and secrets +kubectl apply -f deploy/k8s/namespace.yaml +# Copy and edit secrets +cp deploy/k8s/secrets.example.yaml deploy/k8s/secrets.yaml +# Edit secrets.yaml with base64-encoded values +kubectl apply -f deploy/k8s/secrets.yaml +kubectl apply -f deploy/k8s/app-stack.yaml +kubectl apply -f deploy/k8s/monitoring-stack.yaml +``` + +## Option 2: Helm Chart (Recommended) +```bash +helm upgrade --install finmind deploy/helm/finmind \ + --namespace finmind --create-namespace \ + --set secrets.jwtSecret="$(openssl rand -hex 32)" \ + --set secrets.postgresPassword="$(openssl rand -hex 16)" \ + --set backend.image.repository=your-registry/finmind-backend \ + --set frontend.image.repository=your-registry/finmind-frontend +``` + +### Helm Features +- **HPA**: Auto-scales backend (2-10 replicas based on CPU) +- **Ingress**: TLS via cert-manager with Let's Encrypt +- **ServiceMonitor**: Prometheus-operator integration +- **Health Probes**: Readiness and liveness on all services +- **External Secrets**: Annotations ready for sealed-secrets/external-secrets + +### Custom Values +```bash +helm upgrade --install finmind deploy/helm/finmind \ + -f my-values.yaml --namespace finmind +``` + +## Local Development with Tilt +```bash +tilt up +``` + +## Verification +```bash +kubectl -n finmind get pods +kubectl -n finmind port-forward svc/backend 8000:8000 +curl http://localhost:8000/health +``` diff --git a/docs/deployment/railway.md b/docs/deployment/railway.md new file mode 100644 index 00000000..d57361d2 --- /dev/null +++ b/docs/deployment/railway.md @@ -0,0 +1,72 @@ +# Railway Deployment Guide + +## One-Click Deploy + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/new?template=https://github.com/rohitdash08/FinMind) + +> **Note:** Railway doesn't support multi-service blueprints in a single config file. +> The deploy button creates the backend service. You'll add databases and frontend +> from the Railway dashboard. + +## Step-by-Step Setup + +### 1. Create Project + +```bash +railway login +railway init +``` + +Or create a new project in the [Railway Dashboard](https://railway.app/dashboard). + +### 2. Add Databases + +In the Railway dashboard: +- Click **+ New** → **Database** → **PostgreSQL** +- Click **+ New** → **Database** → **Redis** + +### 3. Add Backend Service + +- Click **+ New** → **GitHub Repo** → select FinMind +- Railway auto-detects the Dockerfile at `packages/backend/Dockerfile` +- In the service **Settings**, set **Root Directory** to `/packages/backend` +- In **Variables**, add: + - `DATABASE_URL` → click "Add Reference" → select the PostgreSQL service → `DATABASE_URL` + - `REDIS_URL` → click "Add Reference" → select the Redis service → `REDIS_URL` + - `JWT_SECRET` → generate a random value (e.g., `openssl rand -hex 32`) + - `LOG_LEVEL` → `INFO` + - `GEMINI_MODEL` → `gemini-1.5-flash` + +> Railway provides `DATABASE_URL` as `postgres://...` — the backend auto-converts this to +> `postgresql+psycopg2://` at startup. No manual conversion needed. + +### 4. Add Frontend Service + +- Click **+ New** → **GitHub Repo** → select FinMind again +- Set **Root Directory** to `/app` +- Railway auto-detects the Dockerfile in `app/` +- In **Variables**, add: + - `VITE_API_URL` → set to the backend service's public URL + +### 5. Deploy + +Railway deploys automatically on push, or manually: + +```bash +railway up +``` + +## Configuration + +The `deploy/railway/railway.json` configures the backend service with: +- Dockerfile-based build +- Health check at `/health` +- Dynamic `$PORT` binding (Railway assigns the port) +- Auto-restart on failure + +## Verification +1. Frontend loads at the Railway-provided URL +2. `GET /health` returns 200 on the backend URL +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/docs/deployment/render.md b/docs/deployment/render.md new file mode 100644 index 00000000..44d7c8e1 --- /dev/null +++ b/docs/deployment/render.md @@ -0,0 +1,57 @@ +# Render Deployment Guide + +## One-Click Deploy + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/rohitdash08/FinMind) + +This creates all services automatically from `render.yaml`. + +## Blueprint Deploy (Manual) + +1. Go to [Render Blueprints](https://dashboard.render.com/blueprints) +2. Click **New Blueprint Instance** +3. Connect your GitHub repo +4. Render auto-detects `render.yaml` and creates all services + +The blueprint provisions: +- **Backend** — Docker web service on port 8000 +- **Frontend** — Static site from `app/dist` +- **PostgreSQL** — Managed database (starter plan) +- **Redis** — Managed key-value store (starter plan) + +## Post-Deploy Setup + +1. After the first deploy, copy the backend service URL (e.g., `https://finmind-backend.onrender.com`) +2. Go to the **finmind-frontend** service → Environment → set `VITE_API_URL` to the backend URL +3. Trigger a manual deploy on the frontend to rebuild with the correct API URL + +## Manual Deploy (Without Blueprint) + +### Backend +1. **New Web Service** → Docker → set Dockerfile path to `packages/backend/Dockerfile` +2. Set environment variables: + - `DATABASE_URL` — from Render PostgreSQL + - `REDIS_URL` — from Render Redis (Key-Value Store) + - `JWT_SECRET` — use "Generate" for a random value +3. Start command: `sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app'` +4. Health check path: `/health` + +### Frontend +1. **New Static Site** → set build command: `cd app && npm ci && npm run build` +2. Publish directory: `app/dist` +3. Add rewrite rule: `/* → /index.html` (SPA support) +4. Set `VITE_API_URL` to backend URL in environment variables + +### Database +1. **New PostgreSQL** → starter plan + +### Redis +1. **New Key-Value Store** (Redis) → starter plan +2. Copy the connection string → set as `REDIS_URL` on the backend + +## Verification +1. Frontend loads in browser +2. `GET /health` returns 200 +3. Register a user (DB connected) +4. Check Redis connectivity (sessions work) +5. Test core modules: expenses, bills, reminders, dashboard, insights diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 00000000..40df6e02 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,10 @@ +build: + docker: + web: packages/backend/Dockerfile +run: + web: + command: + - sh + - -c + - "python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:$PORT wsgi:app" + image: web diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..93001f8d --- /dev/null +++ b/netlify.toml @@ -0,0 +1,18 @@ +[build] + base = "app" + command = "npm ci && npm run build" + publish = "dist" + +[build.environment] + NODE_VERSION = "20" + VITE_API_URL = "https://your-backend-url.com" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" diff --git a/packages/backend/app/config.py b/packages/backend/app/config.py index cf789755..252356a5 100644 --- a/packages/backend/app/config.py +++ b/packages/backend/app/config.py @@ -1,4 +1,4 @@ -from pydantic import Field +from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -8,6 +8,17 @@ class Settings(BaseSettings): ) redis_url: str = Field(default="redis://redis:6379/0") + @field_validator("database_url", mode="before") + @classmethod + def fix_database_scheme(cls, v: str) -> str: + # Many platforms (Heroku, Railway, DO) provide postgres:// URLs. + # SQLAlchemy 1.4+ requires postgresql://. + if v.startswith("postgres://"): + v = v.replace("postgres://", "postgresql+psycopg2://", 1) + elif v.startswith("postgresql://"): + v = v.replace("postgresql://", "postgresql+psycopg2://", 1) + return v + jwt_secret: str = Field(default="dev-secret-change") jwt_access_minutes: int = 15 jwt_refresh_hours: int = 24 diff --git a/render.yaml b/render.yaml new file mode 100644 index 00000000..0349d07e --- /dev/null +++ b/render.yaml @@ -0,0 +1,54 @@ +services: + - type: web + name: finmind-backend + runtime: docker + dockerfilePath: packages/backend/Dockerfile + dockerContext: packages/backend + plan: starter + healthCheckPath: /health + envVars: + - key: DATABASE_URL + fromDatabase: + name: finmind-db + property: connectionString + - key: REDIS_URL + fromService: + name: finmind-redis + type: keyvalue + property: connectionString + - key: JWT_SECRET + generateValue: true + - key: LOG_LEVEL + value: INFO + - key: GEMINI_MODEL + value: gemini-1.5-flash + startCommand: "sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind 0.0.0.0:8000 wsgi:app'" + + - type: web + name: finmind-frontend + runtime: docker + dockerfilePath: app/Dockerfile + dockerContext: app + plan: starter + envVars: + - key: BACKEND_URL + value: https://finmind-backend.onrender.com + # Public URL of the backend service. + # The frontend Docker entrypoint injects this as + # window.__FINMIND_API_URL__ so the browser can reach the API. + # If Render renames the backend (e.g., adds a suffix), + # update this value to match. + + - type: keyvalue + name: finmind-redis + plan: starter + maxmemoryPolicy: allkeys-lru + ipAllowList: + - source: 0.0.0.0/0 + description: everywhere + +databases: + - name: finmind-db + plan: starter + databaseName: finmind + user: finmind diff --git a/scripts/demo-deployment-recording.sh b/scripts/demo-deployment-recording.sh new file mode 100644 index 00000000..5a6e2f18 --- /dev/null +++ b/scripts/demo-deployment-recording.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Demo script for FinMind Universal One-Click Deployment +set -euo pipefail + +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +EMAIL="demo-$(date +%s)@test.com" +PASSWORD='Demo1234!' + +pause() { + sleep "${1:-1}" +} + +type_cmd() { + echo "" + echo -e "${CYAN}\$ ${BOLD}$1${NC}" + pause 0.5 + eval "$1" + pause 1 +} + +echo -e "${GREEN}${BOLD}" +echo "╔══════════════════════════════════════════════════╗" +echo "║ FinMind — Universal One-Click Deployment Demo ║" +echo "╚══════════════════════════════════════════════════╝" +echo -e "${NC}" +pause 1 + +# Show deploy configs +echo -e "${GREEN}▸ 1. Deployment configs for all platforms${NC}" +type_cmd "ls deploy/" +type_cmd "ls deploy/helm/finmind/" +type_cmd "ls deploy/aws/ deploy/gcp/ deploy/azure/ deploy/fly/" +pause 1 + +# Helm lint +echo "" +echo -e "${GREEN}▸ 2. Helm chart validation${NC}" +type_cmd "helm lint deploy/helm/finmind/" +pause 1 + +# Docker compose up +echo "" +echo -e "${GREEN}▸ 3. Docker Compose — full stack deploy${NC}" +type_cmd "cp .env.example .env" +type_cmd "docker compose up -d --build" +pause 3 + +# Wait for healthy +echo "" +echo -e "${GREEN}▸ 4. Waiting for services to be healthy...${NC}" +pause 15 +type_cmd "docker compose ps" +pause 1 + +# Health checks +echo "" +echo -e "${GREEN}▸ 5. Health checks${NC}" +type_cmd "curl -s http://localhost:8000/health | python3 -m json.tool" +type_cmd "curl -sI http://localhost:5173 | head -5" +type_cmd "docker compose exec redis redis-cli ping" +type_cmd "docker compose exec postgres pg_isready -U finmind" +pause 1 + +# Backend logs +echo "" +echo -e "${GREEN}▸ 6. Backend logs — DB init + gunicorn workers${NC}" +type_cmd "docker compose logs backend --tail 15" +pause 1 + +# Auth flow +echo "" +echo -e "${GREEN}▸ 7. Auth flow — register + login${NC}" +type_cmd "curl -s -X POST http://localhost:8000/auth/register -H 'Content-Type: application/json' -d '{\"email\":\"${EMAIL}\",\"password\":\"${PASSWORD}\"}' | python3 -m json.tool" +type_cmd "curl -s -X POST http://localhost:8000/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"${EMAIL}\",\"password\":\"${PASSWORD}\"}' | python3 -m json.tool" +pause 1 + +# Monitoring +echo "" +echo -e "${GREEN}▸ 8. Monitoring stack${NC}" +type_cmd "curl -sI http://localhost:9090 | head -3" +type_cmd "curl -sI http://localhost:3000 | head -3" +pause 1 + +# Deploy script preview +echo "" +echo -e "${GREEN}▸ 9. Interactive deploy script (14 platforms)${NC}" +type_cmd "head -28 scripts/deploy.sh" +pause 1 + +# Clean shutdown +echo "" +echo -e "${GREEN}▸ 10. Clean shutdown${NC}" +type_cmd "docker compose down" +pause 1 + +echo "" +echo -e "${GREEN}${BOLD}✅ All checks passed — deployment verified end-to-end!${NC}" +echo "" diff --git a/scripts/deploy-k8s.sh b/scripts/deploy-k8s.sh index 624e38db..4bd1650f 100644 --- a/scripts/deploy-k8s.sh +++ b/scripts/deploy-k8s.sh @@ -1,6 +1,13 @@ #!/usr/bin/env sh set -eu +if [ ! -f deploy/k8s/secrets.yaml ]; then + echo "WARNING: deploy/k8s/secrets.yaml not found — copying from secrets.example.yaml." + echo " Edit deploy/k8s/secrets.yaml with real credentials before production use, then re-run this script." + cp deploy/k8s/secrets.example.yaml deploy/k8s/secrets.yaml + exit 1 +fi + kubectl apply -f deploy/k8s/namespace.yaml kubectl apply -f deploy/k8s/secrets.yaml kubectl apply -f deploy/k8s/app-stack.yaml diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..2e02af59 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# FinMind — Universal One-Click Deploy Script +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +echo "╔══════════════════════════════════════╗" +echo "║ FinMind Deployment Launcher ║" +echo "╚══════════════════════════════════════╝" +echo "" +echo "Select a deployment platform:" +echo "" +echo " 1) Docker Compose (local)" +echo " 2) Kubernetes (kubectl)" +echo " 3) Helm (Kubernetes)" +echo " 4) Railway" +echo " 5) Heroku" +echo " 6) DigitalOcean App Platform" +echo " 7) DigitalOcean Droplet" +echo " 8) Render" +echo " 9) Fly.io" +echo " 10) AWS ECS (CloudFormation)" +echo " 11) GCP Cloud Run" +echo " 12) Azure Container Apps" +echo " 13) Netlify (frontend only)" +echo " 14) Vercel (frontend only)" +echo "" +read -rp "Enter choice [1-14]: " choice + +case "$choice" in + 1) + echo "Starting Docker Compose..." + cd "$REPO_ROOT" + [ ! -f .env ] && cp .env.example .env && echo "Created .env from .env.example" + docker compose up -d + echo "✅ Running! Frontend: http://localhost:5173 | Backend: http://localhost:8000/health" + ;; + 2) + echo "Deploying to Kubernetes..." + bash "$REPO_ROOT/scripts/deploy-k8s.sh" + ;; + 3) + echo "Deploying with Helm..." + helm upgrade --install finmind "$REPO_ROOT/deploy/helm/finmind" \ + --namespace finmind --create-namespace \ + --set secrets.jwtSecret="$(openssl rand -hex 32)" \ + --set secrets.postgresPassword="$(openssl rand -hex 16)" + echo "✅ Helm release deployed!" + ;; + 4) + echo "Deploy to Railway:" + echo " 1. Install Railway CLI: npm i -g @railway/cli" + echo " 2. railway login && railway init" + echo " 3. Add PostgreSQL and Redis plugins in dashboard" + echo " 4. railway up" + echo "See: deploy/railway/README.md" + ;; + 5) + echo "Deploy to Heroku:" + echo " Option A — One-click deploy button: see docs/deployment/heroku.md" + echo " Option B — CLI:" + echo " heroku create finmind-app" + echo " heroku stack:set container" + echo " heroku addons:create heroku-postgresql:essential-0" + echo " heroku addons:create heroku-redis:mini" + echo " git push heroku main" + echo " Note: heroku.yml and app.json at repo root are auto-detected." + ;; + 6) + echo "Deploy to DigitalOcean App Platform:" + echo " doctl apps create --spec .do/app.yaml" + echo " Or use the DO dashboard and import from GitHub." + ;; + 7) + echo "Deploy to DigitalOcean Droplet:" + echo " On your droplet, run:" + echo " curl -sSL https://raw.githubusercontent.com/rohitdash08/FinMind/main/deploy/droplet/setup.sh | bash" + ;; + 8) + echo "Deploy to Render:" + echo " 1. Push render.yaml to your repo" + echo " 2. Go to https://dashboard.render.com/blueprints" + echo " 3. Connect your GitHub repo — Render auto-detects render.yaml" + ;; + 9) + echo "Deploying to Fly.io..." + bash "$REPO_ROOT/deploy/fly/deploy.sh" + ;; + 10) + echo "Deploy to AWS ECS:" + echo " 1. Build & push images to ECR" + echo " 2. aws cloudformation deploy \\" + echo " --template-file deploy/aws/cloudformation.yaml \\" + echo " --stack-name finmind \\" + echo " --parameter-overrides VpcId= SubnetIds= \\" + echo " BackendImage= FrontendImage= \\" + echo " DatabaseUrl= RedisUrl= JwtSecret= \\" + echo " --capabilities CAPABILITY_NAMED_IAM" + ;; + 11) + echo "Deploy to GCP Cloud Run:" + echo " 1. Create secrets in Secret Manager:" + echo " gcloud secrets create finmind-database-url --data-file=-" + echo " gcloud secrets create finmind-redis-url --data-file=-" + echo " gcloud secrets create finmind-jwt-secret --data-file=-" + echo " 2. Submit build:" + echo " gcloud builds submit --config deploy/gcp/cloudbuild.yaml" + ;; + 12) + echo "Deploy to Azure Container Apps:" + echo " az deployment group create \\" + echo " --resource-group finmind-rg \\" + echo " --template-file deploy/azure/main.bicep \\" + echo " --parameters backendImage= frontendImage= \\" + echo " databaseUrl= redisUrl= jwtSecret=" + ;; + 13) + echo "Deploy frontend to Netlify:" + echo " 1. Connect GitHub repo at https://app.netlify.com" + echo " 2. Netlify auto-detects netlify.toml" + echo " 3. Set VITE_API_URL to your backend URL in site settings" + ;; + 14) + echo "Deploy frontend to Vercel:" + echo " 1. Install Vercel CLI: npm i -g vercel" + echo " 2. vercel --prod" + echo " 3. Set VITE_API_URL environment variable in Vercel dashboard" + ;; + *) + echo "Invalid choice." + exit 1 + ;; +esac diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..c290a010 --- /dev/null +++ b/vercel.json @@ -0,0 +1,16 @@ +{ + "buildCommand": "cd app && npm ci && npm run build", + "outputDirectory": "app/dist", + "framework": null, + "rewrites": [ + { "source": "/(.*)", "destination": "/index.html" } + ], + "headers": [ + { + "source": "/assets/(.*)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ] +}