From 049b12c4a535e21efdf65ca52450010eba09de85 Mon Sep 17 00:00:00 2001 From: Abraham Date: Sun, 15 Mar 2026 08:33:53 -0700 Subject: [PATCH] Bounty Submission: Universal One-Click Deployment (Docker + K8s + Tilt) for FinMind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete production-grade deployment system supporting 12+ platforms: Docker: - Multi-stage Dockerfiles for backend (Python/Flask) and frontend (React/nginx) - Production docker-compose.yml with health checks, resource limits, restart policies - Reverse proxy with rate limiting, security headers, and TLS-ready config - Optimized .dockerignore for minimal build context Kubernetes: - Full Helm chart with parameterized values.yaml - Deployments for backend, frontend, PostgreSQL, and Redis - Services, Ingress with TLS (cert-manager integration) - HPA autoscaling for backend and frontend - PodDisruptionBudgets for zero-downtime deployments - ConfigMaps for non-sensitive config, Secrets for credentials - Health probes (startup, liveness, readiness) on all services - PersistentVolumeClaims for database and cache persistence - ServiceAccount with least-privilege security context Tilt: - Tiltfile for local Kubernetes development - Live-reload for backend Python (file sync + SIGHUP) - Hot-reload for frontend via Vite HMR - Port forwarding and resource grouping Platform Configs: - Railway (railway.toml + railway.json) - Render (render.yaml blueprint with auto-provisioned DB/Redis) - Fly.io (fly.toml for backend + frontend) - GCP Cloud Run (Knative service definition with Secret Manager) - Heroku (Procfile with container deployment) - DigitalOcean App Platform (.do/app.yaml) - AWS ECS Fargate (task definition) + App Runner - Azure Container Apps (containerapp.yaml) - Netlify (netlify.toml — frontend SPA) - Vercel (vercel.json — frontend SPA) Scripts: - deploy.sh — unified CLI for all platforms with --platform flag - setup-local.sh — local dev setup (Docker, Tilt, or bare-metal modes) Documentation: - Complete DEPLOYMENT.md with architecture diagram, per-platform instructions, environment variable reference, and troubleshooting guide Closes #144 --- .dockerignore | 46 ++ deploy/DEPLOYMENT.md | 569 ++++++++++++++++++ deploy/docker/.dockerignore | 51 ++ deploy/docker/backend.Dockerfile | 106 ++++ deploy/docker/docker-compose.prod.yml | 179 ++++++ deploy/docker/frontend.Dockerfile | 58 ++ deploy/docker/nginx-proxy.conf | 117 ++++ deploy/docker/nginx.conf | 73 +++ deploy/helm/finmind/Chart.yaml | 24 + deploy/helm/finmind/templates/_helpers.tpl | 80 +++ .../finmind/templates/backend-deployment.yaml | 135 +++++ .../helm/finmind/templates/backend-hpa.yaml | 48 ++ .../helm/finmind/templates/backend-pdb.yaml | 20 + .../finmind/templates/backend-service.yaml | 20 + deploy/helm/finmind/templates/configmap.yaml | 15 + .../templates/frontend-deployment.yaml | 54 ++ .../helm/finmind/templates/frontend-hpa.yaml | 34 ++ .../finmind/templates/frontend-service.yaml | 20 + deploy/helm/finmind/templates/ingress.yaml | 51 ++ deploy/helm/finmind/templates/namespace.yaml | 9 + .../templates/postgresql-deployment.yaml | 89 +++ .../finmind/templates/postgresql-pvc.yaml | 22 + .../finmind/templates/postgresql-service.yaml | 22 + .../finmind/templates/redis-deployment.yaml | 81 +++ deploy/helm/finmind/templates/redis-pvc.yaml | 22 + .../helm/finmind/templates/redis-service.yaml | 22 + deploy/helm/finmind/templates/secrets.yaml | 41 ++ .../finmind/templates/serviceaccount.yaml | 16 + deploy/helm/finmind/values.yaml | 268 +++++++++ deploy/platforms/aws/apprunner.yaml | 51 ++ deploy/platforms/aws/ecs-task-definition.json | 58 ++ deploy/platforms/azure/containerapp.yaml | 99 +++ deploy/platforms/digitalocean/.do/app.yaml | 108 ++++ deploy/platforms/fly/fly-frontend.toml | 37 ++ deploy/platforms/fly/fly.toml | 73 +++ deploy/platforms/gcp/app.yaml | 96 +++ deploy/platforms/heroku/Procfile | 29 + deploy/platforms/netlify/netlify.toml | 53 ++ deploy/platforms/railway/railway.json | 15 + deploy/platforms/railway/railway.toml | 29 + deploy/platforms/render/render.yaml | 102 ++++ deploy/platforms/vercel/vercel.json | 37 ++ deploy/scripts/deploy.sh | 363 +++++++++++ deploy/scripts/setup-local.sh | 212 +++++++ deploy/tilt/Tiltfile | 330 ++++++++++ 45 files changed, 3984 insertions(+) create mode 100644 .dockerignore create mode 100644 deploy/DEPLOYMENT.md create mode 100644 deploy/docker/.dockerignore create mode 100644 deploy/docker/backend.Dockerfile create mode 100644 deploy/docker/docker-compose.prod.yml create mode 100644 deploy/docker/frontend.Dockerfile create mode 100644 deploy/docker/nginx-proxy.conf create mode 100644 deploy/docker/nginx.conf create mode 100644 deploy/helm/finmind/Chart.yaml create mode 100644 deploy/helm/finmind/templates/_helpers.tpl create mode 100644 deploy/helm/finmind/templates/backend-deployment.yaml create mode 100644 deploy/helm/finmind/templates/backend-hpa.yaml create mode 100644 deploy/helm/finmind/templates/backend-pdb.yaml create mode 100644 deploy/helm/finmind/templates/backend-service.yaml create mode 100644 deploy/helm/finmind/templates/configmap.yaml create mode 100644 deploy/helm/finmind/templates/frontend-deployment.yaml create mode 100644 deploy/helm/finmind/templates/frontend-hpa.yaml create mode 100644 deploy/helm/finmind/templates/frontend-service.yaml create mode 100644 deploy/helm/finmind/templates/ingress.yaml create mode 100644 deploy/helm/finmind/templates/namespace.yaml create mode 100644 deploy/helm/finmind/templates/postgresql-deployment.yaml create mode 100644 deploy/helm/finmind/templates/postgresql-pvc.yaml create mode 100644 deploy/helm/finmind/templates/postgresql-service.yaml create mode 100644 deploy/helm/finmind/templates/redis-deployment.yaml create mode 100644 deploy/helm/finmind/templates/redis-pvc.yaml create mode 100644 deploy/helm/finmind/templates/redis-service.yaml create mode 100644 deploy/helm/finmind/templates/secrets.yaml create mode 100644 deploy/helm/finmind/templates/serviceaccount.yaml create mode 100644 deploy/helm/finmind/values.yaml create mode 100644 deploy/platforms/aws/apprunner.yaml create mode 100644 deploy/platforms/aws/ecs-task-definition.json create mode 100644 deploy/platforms/azure/containerapp.yaml create mode 100644 deploy/platforms/digitalocean/.do/app.yaml create mode 100644 deploy/platforms/fly/fly-frontend.toml create mode 100644 deploy/platforms/fly/fly.toml create mode 100644 deploy/platforms/gcp/app.yaml create mode 100644 deploy/platforms/heroku/Procfile create mode 100644 deploy/platforms/netlify/netlify.toml create mode 100644 deploy/platforms/railway/railway.json create mode 100644 deploy/platforms/railway/railway.toml create mode 100644 deploy/platforms/render/render.yaml create mode 100644 deploy/platforms/vercel/vercel.json create mode 100644 deploy/scripts/deploy.sh create mode 100644 deploy/scripts/setup-local.sh create mode 100644 deploy/tilt/Tiltfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..132efca1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# ============================================================================= +# FinMind — Root .dockerignore +# ============================================================================= +# Applied when building from the repository root (multi-stage Dockerfiles +# in deploy/docker/ reference paths relative to repo root). +# ============================================================================= + +# Version control +.git +.gitignore + +# Dependencies (rebuilt inside container) +**/node_modules +**/__pycache__ +**/*.pyc +**/.pytest_cache + +# Environment and secrets +.env +.env.* +!.env.example + +# IDE +.vscode +.idea +*.swp + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +!packages/backend/app/openapi.yaml +docs/ + +# Test artifacts +**/coverage +**/.coverage +**/htmlcov + +# Monitoring (separate services) +monitoring/ + +# CI/CD +.github/ diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md new file mode 100644 index 00000000..1b8dd97e --- /dev/null +++ b/deploy/DEPLOYMENT.md @@ -0,0 +1,569 @@ +# FinMind — Universal Deployment Guide + +Complete deployment documentation for running FinMind on any platform. + +## Architecture + +``` + +------------------+ + | Load Balancer | + | (nginx / cloud) | + +--------+---------+ + | + +-------------+-------------+ + | | + +------+------+ +------+------+ + | Frontend | | Backend | + | React + nginx| | Flask/Gunicorn| + | (port 80) | | (port 8000) | + +------+------+ +------+------+ + | | + | +------------+------------+ + | | | + | +------+------+ +------+------+ + | | PostgreSQL | | Redis | + | | (5432) | | (6379) | + | +-------------+ +-------------+ + | + Serves static + assets (SPA) + + Request Flow: + Browser --> / --> Frontend (React SPA) + Browser --> /api/* --> Backend (Flask API) + Browser --> /health --> Backend health check + Browser --> /metrics --> Prometheus metrics +``` + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Environment Variables](#environment-variables) +3. [Docker Compose](#docker-compose) +4. [Kubernetes (Helm)](#kubernetes-helm) +5. [Tilt (Local K8s Dev)](#tilt-local-k8s-dev) +6. [Railway](#railway) +7. [Render](#render) +8. [Fly.io](#flyio) +9. [Heroku](#heroku) +10. [DigitalOcean](#digitalocean) +11. [AWS (ECS Fargate / App Runner)](#aws) +12. [GCP Cloud Run](#gcp-cloud-run) +13. [Azure Container Apps](#azure-container-apps) +14. [Netlify (Frontend)](#netlify-frontend) +15. [Vercel (Frontend)](#vercel-frontend) +16. [Troubleshooting](#troubleshooting) + +--- + +## Quick Start + +The fastest way to run FinMind locally: + +```bash +# Clone and enter the repo +git clone https://github.com/rohitdash08/FinMind.git +cd FinMind + +# Run the setup script +chmod +x deploy/scripts/setup-local.sh +./deploy/scripts/setup-local.sh --docker +``` + +Or deploy to any platform with the unified script: + +```bash +chmod +x deploy/scripts/deploy.sh +./deploy/scripts/deploy.sh --platform --env .env +``` + +--- + +## Environment Variables + +All configuration is done via environment variables. Copy `.env.example` to `.env` and edit: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DATABASE_URL` | Yes | `postgresql+psycopg2://finmind:finmind@postgres:5432/finmind` | PostgreSQL connection string | +| `REDIS_URL` | Yes | `redis://redis:6379/0` | Redis connection string | +| `JWT_SECRET` | Yes | `change-me` | Secret key for JWT token signing. **Must be changed in production.** | +| `POSTGRES_USER` | Yes | `finmind` | PostgreSQL username | +| `POSTGRES_PASSWORD` | Yes | `finmind` | PostgreSQL password. **Must be changed in production.** | +| `POSTGRES_DB` | Yes | `finmind` | PostgreSQL database name | +| `LOG_LEVEL` | No | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) | +| `GEMINI_API_KEY` | No | `""` | Google Gemini API key for AI insights | +| `GEMINI_MODEL` | No | `gemini-1.5-flash` | Gemini model identifier | +| `OPENAI_API_KEY` | No | `""` | OpenAI API key (alternative AI provider) | +| `TWILIO_ACCOUNT_SID` | No | `""` | Twilio account SID for WhatsApp notifications | +| `TWILIO_AUTH_TOKEN` | No | `""` | Twilio auth token | +| `TWILIO_WHATSAPP_FROM` | No | `""` | Twilio WhatsApp sender number | +| `EMAIL_FROM` | No | `""` | Sender email for notifications | +| `SMTP_URL` | No | `""` | SMTP connection URL (e.g., `smtp+ssl://user:pass@mail:465`) | +| `VITE_API_URL` | No | `http://localhost:8000` | Backend URL (build-time, baked into frontend) | +| `GUNICORN_WORKERS` | No | `2` | Number of Gunicorn worker processes | +| `GUNICORN_THREADS` | No | `4` | Threads per Gunicorn worker | + +--- + +## Docker Compose + +Production-ready Docker Compose deployment with health checks, resource limits, and a reverse proxy. + +```bash +# 1. Configure environment +cp .env.example .env +# Edit .env with production values (strong passwords, real JWT secret) + +# 2. Build and start +docker compose -f deploy/docker/docker-compose.prod.yml --env-file .env up -d --build + +# 3. Verify +curl http://localhost/health # Backend health +curl http://localhost # Frontend +``` + +**Files:** +- `deploy/docker/docker-compose.prod.yml` — Production compose file +- `deploy/docker/backend.Dockerfile` — Multi-stage backend image +- `deploy/docker/frontend.Dockerfile` — Multi-stage frontend image +- `deploy/docker/nginx-proxy.conf` — Reverse proxy config +- `deploy/docker/nginx.conf` — Frontend nginx config + +**Stopping:** +```bash +docker compose -f deploy/docker/docker-compose.prod.yml down +# To also remove volumes (data): +docker compose -f deploy/docker/docker-compose.prod.yml down -v +``` + +--- + +## Kubernetes (Helm) + +Full Helm chart with deployments, services, ingress, HPA, PVCs, health probes, and TLS. + +### Prerequisites +- `kubectl` configured to connect to your cluster +- `helm` v3 installed +- An Ingress Controller (e.g., nginx-ingress) installed in the cluster +- cert-manager installed (for automatic TLS) + +### Deploy + +```bash +# Install with required secrets +helm upgrade --install finmind deploy/helm/finmind \ + --namespace finmind \ + --create-namespace \ + --set secrets.POSTGRES_PASSWORD="$(openssl rand -hex 16)" \ + --set secrets.JWT_SECRET="$(openssl rand -hex 32)" \ + --set ingress.hosts[0].host=finmind.yourdomain.com \ + --set ingress.tls[0].hosts[0]=finmind.yourdomain.com + +# Check status +kubectl get pods -n finmind +kubectl get ingress -n finmind +``` + +### Custom Values + +Create a `values-prod.yaml` to override defaults: + +```yaml +backend: + replicaCount: 3 + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 1Gi + +ingress: + hosts: + - host: finmind.yourdomain.com + paths: + - path: / + pathType: Prefix + service: frontend + - path: /api + pathType: Prefix + service: backend +``` + +```bash +helm upgrade --install finmind deploy/helm/finmind -f values-prod.yaml \ + --set secrets.POSTGRES_PASSWORD="..." \ + --set secrets.JWT_SECRET="..." +``` + +### Using External Database + +To use a managed PostgreSQL (RDS, Cloud SQL, etc.): + +```bash +helm upgrade --install finmind deploy/helm/finmind \ + --set postgresql.enabled=false \ + --set secrets.POSTGRES_PASSWORD="..." \ + --set secrets.JWT_SECRET="..." \ + # Override DATABASE_URL in backend config +``` + +**Files:** +- `deploy/helm/finmind/Chart.yaml` +- `deploy/helm/finmind/values.yaml` +- `deploy/helm/finmind/templates/` — All K8s resource templates + +--- + +## Tilt (Local K8s Dev) + +Tilt provides a fast inner-loop development experience on local Kubernetes with hot-reload. + +### Prerequisites +- Docker Desktop with Kubernetes, minikube, kind, or k3d +- [Tilt](https://docs.tilt.dev/install.html) installed + +### Run + +```bash +cd deploy/tilt +tilt up +``` + +Open http://localhost:10350 to see the Tilt dashboard. + +### Services in Tilt + +| Service | Port | Description | +|---------|------|-------------| +| Frontend | 5173 | Vite dev server with HMR | +| Backend | 8000 | Flask with live-reload | +| PostgreSQL | 5432 | Local database | +| Redis | 6379 | Cache | + +### How Live Reload Works + +- **Backend**: Python files are synced into the container. Gunicorn receives SIGHUP to reload. +- **Frontend**: Source files sync into the container. Vite HMR handles the rest instantly. +- **Dependencies**: If `requirements.txt` or `package.json` changes, a full image rebuild is triggered. + +**Files:** +- `deploy/tilt/Tiltfile` + +--- + +## Railway + +```bash +# 1. Install Railway CLI +npm i -g @railway/cli + +# 2. Login and init +railway login +railway init + +# 3. Add PostgreSQL and Redis plugins in the Railway dashboard +# (Railway auto-sets DATABASE_URL and REDIS_URL) + +# 4. Set secrets +railway variables set JWT_SECRET="$(openssl rand -hex 32)" + +# 5. Deploy +railway up +``` + +**Files:** `deploy/platforms/railway/railway.toml`, `deploy/platforms/railway/railway.json` + +--- + +## Render + +Render uses a Blueprint file for one-click infrastructure setup. + +```bash +# 1. Push repo to GitHub +# 2. Go to https://dashboard.render.com/blueprints +# 3. Click "New Blueprint Instance" +# 4. Connect your repo — Render auto-detects render.yaml +# 5. Set JWT_SECRET and API keys in the dashboard +``` + +Render automatically provisions PostgreSQL, Redis, TLS, and a `.onrender.com` domain. + +**Files:** `deploy/platforms/render/render.yaml` + +--- + +## Fly.io + +```bash +# 1. Install flyctl +curl -L https://fly.io/install.sh | sh + +# 2. Login +fly auth login + +# 3. Create backend app +fly apps create finmind-backend + +# 4. Provision database and cache +fly postgres create --name finmind-db +fly postgres attach finmind-db --app finmind-backend +fly redis create --name finmind-redis + +# 5. Set secrets +fly secrets set JWT_SECRET="$(openssl rand -hex 32)" --app finmind-backend +fly secrets set REDIS_URL="" --app finmind-backend + +# 6. Deploy backend +fly deploy --config deploy/platforms/fly/fly.toml + +# 7. Deploy frontend +fly apps create finmind-frontend +fly deploy --config deploy/platforms/fly/fly-frontend.toml +``` + +**Files:** `deploy/platforms/fly/fly.toml`, `deploy/platforms/fly/fly-frontend.toml` + +--- + +## Heroku + +```bash +# 1. Install Heroku CLI +# 2. Login +heroku login + +# 3. Create app with addons +heroku create finmind-backend +heroku addons:create heroku-postgresql:essential-0 +heroku addons:create heroku-redis:mini + +# 4. Set config +heroku config:set JWT_SECRET="$(openssl rand -hex 32)" +heroku config:set LOG_LEVEL=INFO +heroku config:set GEMINI_MODEL=gemini-1.5-flash + +# 5. Deploy via container +heroku container:push web --app finmind-backend \ + --dockerfile deploy/docker/backend.Dockerfile +heroku container:release web --app finmind-backend +``` + +**Files:** `deploy/platforms/heroku/Procfile` + +--- + +## DigitalOcean + +### App Platform + +```bash +# 1. Install doctl +# 2. Deploy +doctl apps create --spec deploy/platforms/digitalocean/.do/app.yaml + +# 3. Set secrets in the DigitalOcean dashboard +``` + +### Droplet + +Use the unified deploy script for manual Droplet setup: + +```bash +./deploy/scripts/deploy.sh --platform docker --env .env +``` + +**Files:** `deploy/platforms/digitalocean/.do/app.yaml` + +--- + +## AWS + +### ECS Fargate + +```bash +# 1. Create ECR repository +aws ecr create-repository --repository-name finmind-backend + +# 2. Build and push +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com +docker build -f deploy/docker/backend.Dockerfile -t finmind-backend . +docker tag finmind-backend:latest ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest +docker push ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest + +# 3. Create RDS PostgreSQL and ElastiCache Redis +# 4. Store secrets in AWS Secrets Manager +# 5. Register task definition +aws ecs register-task-definition --cli-input-json file://deploy/platforms/aws/ecs-task-definition.json + +# 6. Create service +aws ecs create-service --cluster finmind --service-name finmind-backend \ + --task-definition finmind-backend --desired-count 2 --launch-type FARGATE +``` + +### App Runner + +```bash +aws apprunner create-service --cli-input-yaml file://deploy/platforms/aws/apprunner.yaml +``` + +**Files:** `deploy/platforms/aws/ecs-task-definition.json`, `deploy/platforms/aws/apprunner.yaml` + +--- + +## GCP Cloud Run + +```bash +# 1. Set project +gcloud config set project YOUR_PROJECT_ID + +# 2. Enable APIs +gcloud services enable run.googleapis.com sqladmin.googleapis.com redis.googleapis.com + +# 3. Build and deploy +gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/finmind-backend . +gcloud run deploy finmind-backend \ + --image gcr.io/YOUR_PROJECT_ID/finmind-backend \ + --region us-central1 --port 8000 \ + --min-instances 1 --max-instances 10 \ + --memory 512Mi --cpu 1 + +# 4. Create Cloud SQL PostgreSQL and Memorystore Redis +# 5. Store secrets in Secret Manager +# 6. Update service with secret bindings (see app.yaml) +``` + +**Files:** `deploy/platforms/gcp/app.yaml` + +--- + +## Azure Container Apps + +```bash +# 1. Create resource group and environment +az group create --name finmind-rg --location eastus +az containerapp env create --name finmind-env --resource-group finmind-rg + +# 2. Create database and cache +az postgres flexible-server create --name finmind-db --resource-group finmind-rg +az redis create --name finmind-redis --resource-group finmind-rg --sku Basic --vm-size C0 + +# 3. Build and push to ACR +az acr create --name finmindacr --resource-group finmind-rg --sku Basic +az acr build --registry finmindacr --image finmind-backend:latest -f deploy/docker/backend.Dockerfile . + +# 4. Deploy +az containerapp create --yaml deploy/platforms/azure/containerapp.yaml +``` + +**Files:** `deploy/platforms/azure/containerapp.yaml` + +--- + +## Netlify (Frontend) + +Netlify deploys only the React SPA frontend. The backend must be hosted separately. + +```bash +# 1. Install Netlify CLI +npm i -g netlify-cli + +# 2. Set VITE_API_URL in Netlify dashboard to your backend URL +# 3. Deploy +cd app +npm ci && npm run build +netlify deploy --prod --dir=dist +``` + +**Files:** `deploy/platforms/netlify/netlify.toml` + +--- + +## Vercel (Frontend) + +Vercel deploys only the React SPA frontend. + +```bash +# 1. Install Vercel CLI +npm i -g vercel + +# 2. Set VITE_API_URL in Vercel dashboard +# 3. Deploy +cd app +vercel --prod +``` + +**Files:** `deploy/platforms/vercel/vercel.json` + +--- + +## Troubleshooting + +### Backend won't start: "database not initialized" + +The backend runs `flask init-db` on startup. If PostgreSQL is not ready yet, this will fail. Ensure: +- PostgreSQL health check passes before backend starts +- `DATABASE_URL` is correctly formatted +- Database user has permissions to create tables + +```bash +# Check PostgreSQL is ready +docker compose exec postgres pg_isready -U finmind + +# Manually initialize +docker compose exec backend python -m flask --app wsgi:app init-db +``` + +### Frontend shows blank page + +The React SPA needs `VITE_API_URL` set at **build time** (it is baked into the JS bundle): + +```bash +# Rebuild frontend with correct API URL +docker compose build --build-arg VITE_API_URL=https://api.yourdomain.com frontend +``` + +### Health check fails on port 8080 + +Some platforms (Fly.io, Cloud Run) expect port 8080. Set `GUNICORN_BIND=0.0.0.0:8080` in your environment. + +### Redis connection refused + +Verify the `REDIS_URL` environment variable points to the correct host: +- Docker Compose: `redis://redis:6379/0` +- Kubernetes: `redis://finmind-redis:6379/0` (service name) +- Managed Redis: use the connection string provided by your cloud provider + +### Kubernetes pods in CrashLoopBackOff + +```bash +# Check pod logs +kubectl logs -n finmind --previous + +# Check events +kubectl describe pod -n finmind + +# Common causes: +# - Secret not created (POSTGRES_PASSWORD, JWT_SECRET are required) +# - Database not reachable +# - Image pull failures +``` + +### TLS/HTTPS not working on Kubernetes + +Ensure: +1. cert-manager is installed: `kubectl get pods -n cert-manager` +2. A ClusterIssuer exists: `kubectl get clusterissuer letsencrypt-prod` +3. DNS points to the ingress controller's external IP +4. The ingress annotation `cert-manager.io/cluster-issuer` matches your issuer name + +### "Permission denied" on deploy scripts + +```bash +chmod +x deploy/scripts/deploy.sh deploy/scripts/setup-local.sh +``` diff --git a/deploy/docker/.dockerignore b/deploy/docker/.dockerignore new file mode 100644 index 00000000..27ddb5f3 --- /dev/null +++ b/deploy/docker/.dockerignore @@ -0,0 +1,51 @@ +# ============================================================================= +# FinMind — Docker build context exclusions +# ============================================================================= +# Keep the build context small and avoid leaking secrets into images. + +# Version control +.git +.gitignore + +# Dependencies (rebuilt inside the container) +node_modules +app/node_modules +__pycache__ +*.pyc +*.pyo +.pytest_cache + +# Environment and secrets +.env +.env.* +!.env.example + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Documentation (not needed in runtime image) +*.md +docs/ +LICENSE + +# Testing artifacts +coverage/ +.coverage +htmlcov/ + +# Monitoring configs (separate compose service) +monitoring/ + +# Deploy configs (not part of the app image) +deploy/ + +# CI/CD +.github/ diff --git a/deploy/docker/backend.Dockerfile b/deploy/docker/backend.Dockerfile new file mode 100644 index 00000000..42da7dd0 --- /dev/null +++ b/deploy/docker/backend.Dockerfile @@ -0,0 +1,106 @@ +# ============================================================================= +# FinMind Backend — Production Multi-Stage Dockerfile +# ============================================================================= +# Stage 1: Build dependencies in a full image (compile psycopg2, etc.) +# Stage 2: Copy into slim runtime image for minimal attack surface +# ============================================================================= + +# --------------------------------------------------------------------------- +# Stage 1 — Builder +# --------------------------------------------------------------------------- +FROM python:3.11-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /build + +# Install build-time system dependencies required by psycopg2 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +# Install Python dependencies into a virtual-env so we can copy it cleanly +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY packages/backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# --------------------------------------------------------------------------- +# Stage 2 — Runtime +# --------------------------------------------------------------------------- +FROM python:3.11-slim AS runtime + +# Labels for container registries +LABEL org.opencontainers.image.title="finmind-backend" \ + org.opencontainers.image.description="FinMind backend API server" \ + org.opencontainers.image.source="https://github.com/rohitdash08/FinMind" + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + # Gunicorn tuning defaults (overridable at runtime) + GUNICORN_WORKERS=2 \ + GUNICORN_THREADS=4 \ + GUNICORN_BIND="0.0.0.0:8000" \ + GUNICORN_TIMEOUT=120 \ + GUNICORN_GRACEFUL_TIMEOUT=30 \ + # Prometheus multi-process directory + PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc + +# Runtime-only system dependency: libpq for psycopg2 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libpq5 \ + curl \ + tini && \ + rm -rf /var/lib/apt/lists/* && \ + # Create non-root user + groupadd -r finmind && \ + useradd -r -g finmind -d /app -s /sbin/nologin finmind + +# Copy virtual-env from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +WORKDIR /app + +# Copy application code +COPY packages/backend/app ./app +COPY packages/backend/wsgi.py ./wsgi.py + +# Prepare prometheus multiproc directory +RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR} && \ + chown -R finmind:finmind /app ${PROMETHEUS_MULTIPROC_DIR} + +# Switch to non-root user +USER finmind + +EXPOSE 8000 + +# Health check — uses the /health endpoint built into the Flask app +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD curl -sf http://localhost:8000/health || exit 1 + +# Use tini as PID 1 for proper signal handling +ENTRYPOINT ["tini", "--"] + +# Initialize the database schema, then start gunicorn +CMD ["sh", "-c", "\ + python -m flask --app wsgi:app init-db && \ + rm -rf ${PROMETHEUS_MULTIPROC_DIR}/* && \ + exec gunicorn \ + --workers=${GUNICORN_WORKERS} \ + --threads=${GUNICORN_THREADS} \ + --bind=${GUNICORN_BIND} \ + --timeout=${GUNICORN_TIMEOUT} \ + --graceful-timeout=${GUNICORN_GRACEFUL_TIMEOUT} \ + --access-logfile=- \ + --error-logfile=- \ + --log-level=info \ + --forwarded-allow-ips='*' \ + wsgi:app"] diff --git a/deploy/docker/docker-compose.prod.yml b/deploy/docker/docker-compose.prod.yml new file mode 100644 index 00000000..1f28bf02 --- /dev/null +++ b/deploy/docker/docker-compose.prod.yml @@ -0,0 +1,179 @@ +# ============================================================================= +# FinMind — Production Docker Compose +# ============================================================================= +# Usage: +# cp .env.example .env # edit secrets +# docker compose -f deploy/docker/docker-compose.prod.yml up -d --build +# +# Services: postgres, redis, backend, frontend, nginx-proxy +# ============================================================================= + +services: + # --------------------------------------------------------------------------- + # PostgreSQL 16 + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - finmind-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + deploy: + resources: + limits: + cpus: "1.0" + memory: 512M + reservations: + cpus: "0.25" + memory: 128M + # Do not expose postgres to the host in production + expose: + - "5432" + + # --------------------------------------------------------------------------- + # Redis 7 + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + restart: always + command: > + redis-server + --maxmemory 128mb + --maxmemory-policy allkeys-lru + --appendonly yes + --save 60 1000 + volumes: + - redisdata:/data + networks: + - finmind-net + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + deploy: + resources: + limits: + cpus: "0.5" + memory: 256M + reservations: + cpus: "0.1" + memory: 64M + expose: + - "6379" + + # --------------------------------------------------------------------------- + # Backend (Flask + Gunicorn) + # --------------------------------------------------------------------------- + backend: + build: + context: ../../ + dockerfile: deploy/docker/backend.Dockerfile + restart: always + env_file: + - ../../.env + environment: + DATABASE_URL: "postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}" + REDIS_URL: "redis://redis:6379/0" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - finmind-net + expose: + - "8000" + deploy: + resources: + limits: + cpus: "1.0" + memory: 512M + reservations: + cpus: "0.25" + memory: 128M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 5 + window: 60s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + # --------------------------------------------------------------------------- + # Frontend (Vite build served by nginx) + # --------------------------------------------------------------------------- + frontend: + build: + context: ../../ + dockerfile: deploy/docker/frontend.Dockerfile + args: + VITE_API_URL: ${VITE_API_URL:-/api} + restart: always + networks: + - finmind-net + expose: + - "80" + deploy: + resources: + limits: + cpus: "0.5" + memory: 128M + reservations: + cpus: "0.1" + memory: 32M + + # --------------------------------------------------------------------------- + # Reverse Proxy — nginx routing frontend + backend under one host + # --------------------------------------------------------------------------- + nginx-proxy: + image: nginx:1.27-alpine + restart: always + ports: + - "${LISTEN_PORT:-80}:80" + - "${LISTEN_PORT_TLS:-443}:443" + volumes: + - ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf:ro + # Mount TLS certs if available (optional — comment out if not using TLS) + # - /etc/letsencrypt/live/${DOMAIN}/fullchain.pem:/etc/nginx/ssl/cert.pem:ro + # - /etc/letsencrypt/live/${DOMAIN}/privkey.pem:/etc/nginx/ssl/key.pem:ro + depends_on: + - backend + - frontend + networks: + - finmind-net + healthcheck: + test: ["CMD-SHELL", "wget -qO /dev/null http://localhost/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + deploy: + resources: + limits: + cpus: "0.5" + memory: 128M + +networks: + finmind-net: + driver: bridge + +volumes: + pgdata: + driver: local + redisdata: + driver: local diff --git a/deploy/docker/frontend.Dockerfile b/deploy/docker/frontend.Dockerfile new file mode 100644 index 00000000..f4d68eb9 --- /dev/null +++ b/deploy/docker/frontend.Dockerfile @@ -0,0 +1,58 @@ +# ============================================================================= +# FinMind Frontend — Production Multi-Stage Dockerfile +# ============================================================================= +# Stage 1: Install deps + build the Vite/React app +# Stage 2: Serve static assets via hardened nginx +# ============================================================================= + +# --------------------------------------------------------------------------- +# Stage 1 — Builder +# --------------------------------------------------------------------------- +FROM node:20-alpine AS builder + +WORKDIR /app + +# Accept build-time API URL (baked into the JS bundle by Vite) +ARG VITE_API_URL=http://localhost:8000 +ENV VITE_API_URL=${VITE_API_URL} + +# Install dependencies first (layer cache optimisation) +COPY app/package.json app/package-lock.json ./ +RUN npm ci --ignore-scripts + +# Copy source and build +COPY app/ . +RUN npm run build + +# --------------------------------------------------------------------------- +# Stage 2 — Production nginx +# --------------------------------------------------------------------------- +FROM nginx:1.27-alpine AS runtime + +LABEL org.opencontainers.image.title="finmind-frontend" \ + org.opencontainers.image.description="FinMind frontend SPA served by nginx" \ + org.opencontainers.image.source="https://github.com/rohitdash08/FinMind" + +# Remove default nginx content +RUN rm -rf /usr/share/nginx/html/* /etc/nginx/conf.d/default.conf + +# Copy our production nginx configuration +COPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets from the builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Create cache and temp directories with correct permissions for non-root +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown nginx:nginx /var/run/nginx.pid + +EXPOSE 80 + +# Health check — verify nginx is serving content +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO /dev/null http://localhost:80/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/deploy/docker/nginx-proxy.conf b/deploy/docker/nginx-proxy.conf new file mode 100644 index 00000000..e6076dc6 --- /dev/null +++ b/deploy/docker/nginx-proxy.conf @@ -0,0 +1,117 @@ +# ============================================================================= +# FinMind — Production Reverse Proxy Configuration +# ============================================================================= +# Routes /api/* to backend, everything else to frontend SPA. +# Handles WebSocket upgrades, proxy headers, and rate limiting. +# ============================================================================= + +upstream backend_upstream { + server backend:8000; + keepalive 32; +} + +upstream frontend_upstream { + server frontend:80; + keepalive 16; +} + +# Rate limiting zone — 10 req/s per IP for API endpoints +limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + +server { + listen 80; + server_name _; + + # Redirect to HTTPS in production (uncomment when TLS is configured) + # return 301 https://$host$request_uri; + + # ----------------------------------------------------------------------- + # Security headers + # ----------------------------------------------------------------------- + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # ----------------------------------------------------------------------- + # Backend API — proxy /api/* requests to the Flask backend + # ----------------------------------------------------------------------- + location /api/ { + limit_req zone=api_limit burst=20 nodelay; + + # Strip /api prefix so the backend sees clean paths + rewrite ^/api/(.*) /$1 break; + + proxy_pass http://backend_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + # Timeouts + proxy_connect_timeout 10s; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + + # Direct access to health and metrics (no /api prefix needed) + location = /health { + proxy_pass http://backend_upstream/health; + proxy_set_header Host $host; + access_log off; + } + + location = /metrics { + proxy_pass http://backend_upstream/metrics; + proxy_set_header Host $host; + access_log off; + } + + # ----------------------------------------------------------------------- + # Frontend SPA — everything else goes to the React app + # ----------------------------------------------------------------------- + location / { + proxy_pass http://frontend_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # ----------------------------------------------------------------------- + # Nginx status for monitoring + # ----------------------------------------------------------------------- + location /nginx_status { + stub_status; + allow 127.0.0.1; + allow 10.0.0.0/8; + allow 172.16.0.0/12; + allow 192.168.0.0/16; + deny all; + access_log off; + } +} + +# ============================================================================= +# HTTPS server block (uncomment when TLS certs are mounted) +# ============================================================================= +# server { +# listen 443 ssl http2; +# server_name ${DOMAIN}; +# +# ssl_certificate /etc/nginx/ssl/cert.pem; +# ssl_certificate_key /etc/nginx/ssl/key.pem; +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers HIGH:!aNULL:!MD5; +# ssl_prefer_server_ciphers on; +# ssl_session_cache shared:SSL:10m; +# ssl_session_timeout 10m; +# +# # HSTS +# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +# +# # Same location blocks as above ... +# } diff --git a/deploy/docker/nginx.conf b/deploy/docker/nginx.conf new file mode 100644 index 00000000..3a5aa601 --- /dev/null +++ b/deploy/docker/nginx.conf @@ -0,0 +1,73 @@ +# ============================================================================= +# FinMind Frontend — Production nginx Configuration +# ============================================================================= +# - SPA fallback (all non-file routes -> index.html) +# - Aggressive caching for hashed static assets +# - Security headers +# - Gzip compression +# - Upstream proxy to backend API +# ============================================================================= + +# Gzip compression for text-based responses +gzip on; +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_min_length 256; +gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + image/svg+xml; + +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # ----------------------------------------------------------------------- + # Security headers + # ----------------------------------------------------------------------- + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # ----------------------------------------------------------------------- + # SPA fallback — serve index.html for any route the browser requests + # ----------------------------------------------------------------------- + location / { + try_files $uri $uri/ /index.html; + } + + # ----------------------------------------------------------------------- + # Static assets — long cache for Vite-hashed files + # ----------------------------------------------------------------------- + location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|woff2?|ttf|eot|webp|avif)$ { + expires 1y; + access_log off; + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + # ----------------------------------------------------------------------- + # Health endpoint for load balancers / orchestrators + # ----------------------------------------------------------------------- + location = /nginx-health { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } + + # Disable access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} diff --git a/deploy/helm/finmind/Chart.yaml b/deploy/helm/finmind/Chart.yaml new file mode 100644 index 00000000..3d48c90b --- /dev/null +++ b/deploy/helm/finmind/Chart.yaml @@ -0,0 +1,24 @@ +# ============================================================================= +# FinMind — Helm Chart Metadata +# ============================================================================= +apiVersion: v2 +name: finmind +description: >- + Production Helm chart for FinMind — a personal finance management platform + with Flask backend, React frontend, PostgreSQL, and Redis. +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - finmind + - finance + - flask + - react + - postgresql + - redis +home: https://github.com/rohitdash08/FinMind +sources: + - https://github.com/rohitdash08/FinMind +maintainers: + - name: FinMind Contributors + url: https://github.com/rohitdash08/FinMind diff --git a/deploy/helm/finmind/templates/_helpers.tpl b/deploy/helm/finmind/templates/_helpers.tpl new file mode 100644 index 00000000..9d20abd9 --- /dev/null +++ b/deploy/helm/finmind/templates/_helpers.tpl @@ -0,0 +1,80 @@ +{{/* +============================================================================= +FinMind Helm Chart — Template Helpers +============================================================================= +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "finmind.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "finmind.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Chart label for all resources. +*/}} +{{- define "finmind.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels applied to every resource. +*/}} +{{- define "finmind.labels" -}} +helm.sh/chart: {{ include "finmind.chart" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: finmind +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} + +{{/* +Selector labels for a specific component. +Usage: {{ include "finmind.selectorLabels" (dict "component" "backend" "context" .) }} +*/}} +{{- define "finmind.selectorLabels" -}} +app.kubernetes.io/name: {{ include "finmind.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +Construct the DATABASE_URL from secret references. +*/}} +{{- define "finmind.databaseUrl" -}} +postgresql+psycopg2://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@{{ include "finmind.fullname" . }}-postgresql:5432/$(POSTGRES_DB) +{{- end }} + +{{/* +Redis URL pointing to the in-cluster Redis service. +*/}} +{{- define "finmind.redisUrl" -}} +redis://{{ include "finmind.fullname" . }}-redis:6379/0 +{{- end }} + +{{/* +Service account name. +*/}} +{{- define "finmind.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "finmind.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/backend-deployment.yaml b/deploy/helm/finmind/templates/backend-deployment.yaml new file mode 100644 index 00000000..99515ef4 --- /dev/null +++ b/deploy/helm/finmind/templates/backend-deployment.yaml @@ -0,0 +1,135 @@ +# ============================================================================= +# FinMind — Backend Deployment +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-backend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 4 }} +spec: + {{- if not .Values.backend.autoscaling.enabled }} + replicas: {{ .Values.backend.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 6 }} + strategy: + {{- toYaml .Values.backend.strategy | nindent 4 }} + template: + metadata: + labels: + {{- include "finmind.labels" . | nindent 8 }} + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 8 }} + annotations: + # Force rollout on config/secret changes + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "finmind.serviceAccountName" . }} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + # Graceful shutdown period matching gunicorn graceful timeout + terminationGracePeriodSeconds: 45 + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + # --- Secrets (injected from Secret resource) --- + - 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: DATABASE_URL + value: {{ include "finmind.databaseUrl" . | quote }} + # --- ConfigMap values --- + - 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 + # --- Optional secrets (only if provided) --- + {{- if .Values.secrets.GEMINI_API_KEY }} + - name: GEMINI_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: GEMINI_API_KEY + {{- end }} + {{- if .Values.secrets.OPENAI_API_KEY }} + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "finmind.fullname" . }}-secrets + key: OPENAI_API_KEY + {{- end }} + 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 && \ + exec gunicorn \ + --workers=${GUNICORN_WORKERS:-2} \ + --threads=${GUNICORN_THREADS:-4} \ + --bind=0.0.0.0:8000 \ + --timeout=120 \ + --graceful-timeout=30 \ + --access-logfile=- \ + --error-logfile=- \ + --forwarded-allow-ips='*' \ + wsgi:app + # --- Health probes --- + startupProbe: + {{- toYaml .Values.backend.startupProbe | nindent 12 }} + livenessProbe: + {{- toYaml .Values.backend.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.backend.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/deploy/helm/finmind/templates/backend-hpa.yaml b/deploy/helm/finmind/templates/backend-hpa.yaml new file mode 100644 index 00000000..6de66740 --- /dev/null +++ b/deploy/helm/finmind/templates/backend-hpa.yaml @@ -0,0 +1,48 @@ +# ============================================================================= +# FinMind — Backend Horizontal Pod Autoscaler +# ============================================================================= +{{- if .Values.backend.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "finmind.fullname" . }}-backend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "finmind.fullname" . }}-backend + minReplicas: {{ .Values.backend.autoscaling.minReplicas }} + maxReplicas: {{ .Values.backend.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.backend.autoscaling.targetCPUUtilizationPercentage }} + {{- if .Values.backend.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.backend.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Pods + value: 2 + periodSeconds: 60 +{{- end }} diff --git a/deploy/helm/finmind/templates/backend-pdb.yaml b/deploy/helm/finmind/templates/backend-pdb.yaml new file mode 100644 index 00000000..6b867567 --- /dev/null +++ b/deploy/helm/finmind/templates/backend-pdb.yaml @@ -0,0 +1,20 @@ +# ============================================================================= +# FinMind — Backend Pod Disruption Budget +# ============================================================================= +# Ensures at least one backend pod is always available during voluntary +# disruptions (node drains, upgrades, etc.) for zero-downtime deployments. +# ============================================================================= +{{- if .Values.backend.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "finmind.fullname" . }}-backend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} +spec: + minAvailable: {{ .Values.backend.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 6 }} +{{- end }} diff --git a/deploy/helm/finmind/templates/backend-service.yaml b/deploy/helm/finmind/templates/backend-service.yaml new file mode 100644 index 00000000..c8ed1542 --- /dev/null +++ b/deploy/helm/finmind/templates/backend-service.yaml @@ -0,0 +1,20 @@ +# ============================================================================= +# FinMind — Backend Service +# ============================================================================= +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-backend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 4 }} +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "finmind.selectorLabels" (dict "component" "backend" "context" .) | nindent 4 }} diff --git a/deploy/helm/finmind/templates/configmap.yaml b/deploy/helm/finmind/templates/configmap.yaml new file mode 100644 index 00000000..d7fe97ec --- /dev/null +++ b/deploy/helm/finmind/templates/configmap.yaml @@ -0,0 +1,15 @@ +# ============================================================================= +# FinMind — ConfigMap (non-sensitive configuration) +# ============================================================================= +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "finmind.fullname" . }}-config + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.backend.config }} + {{ $key }}: {{ $value | quote }} + {{- end }} + REDIS_URL: {{ include "finmind.redisUrl" . | quote }} diff --git a/deploy/helm/finmind/templates/frontend-deployment.yaml b/deploy/helm/finmind/templates/frontend-deployment.yaml new file mode 100644 index 00000000..59cb8a3b --- /dev/null +++ b/deploy/helm/finmind/templates/frontend-deployment.yaml @@ -0,0 +1,54 @@ +# ============================================================================= +# FinMind — Frontend Deployment +# ============================================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-frontend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 4 }} +spec: + {{- if not .Values.frontend.autoscaling.enabled }} + replicas: {{ .Values.frontend.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 6 }} + strategy: + {{- toYaml .Values.frontend.strategy | nindent 4 }} + template: + metadata: + labels: + {{- include "finmind.labels" . | nindent 8 }} + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 8 }} + spec: + serviceAccountName: {{ include "finmind.serviceAccountName" . }} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: frontend + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + {{- toYaml .Values.frontend.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.frontend.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: false + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE diff --git a/deploy/helm/finmind/templates/frontend-hpa.yaml b/deploy/helm/finmind/templates/frontend-hpa.yaml new file mode 100644 index 00000000..4069221a --- /dev/null +++ b/deploy/helm/finmind/templates/frontend-hpa.yaml @@ -0,0 +1,34 @@ +# ============================================================================= +# FinMind — Frontend Horizontal Pod Autoscaler +# ============================================================================= +{{- if .Values.frontend.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "finmind.fullname" . }}-frontend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "finmind.fullname" . }}-frontend + minReplicas: {{ .Values.frontend.autoscaling.minReplicas }} + maxReplicas: {{ .Values.frontend.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.frontend.autoscaling.targetCPUUtilizationPercentage }} + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 +{{- end }} diff --git a/deploy/helm/finmind/templates/frontend-service.yaml b/deploy/helm/finmind/templates/frontend-service.yaml new file mode 100644 index 00000000..0330d877 --- /dev/null +++ b/deploy/helm/finmind/templates/frontend-service.yaml @@ -0,0 +1,20 @@ +# ============================================================================= +# FinMind — Frontend Service +# ============================================================================= +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-frontend + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 4 }} +spec: + type: {{ .Values.frontend.service.type }} + ports: + - port: {{ .Values.frontend.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "finmind.selectorLabels" (dict "component" "frontend" "context" .) | nindent 4 }} diff --git a/deploy/helm/finmind/templates/ingress.yaml b/deploy/helm/finmind/templates/ingress.yaml new file mode 100644 index 00000000..700ad7df --- /dev/null +++ b/deploy/helm/finmind/templates/ingress.yaml @@ -0,0 +1,51 @@ +# ============================================================================= +# FinMind — Ingress (TLS-enabled, multi-path routing) +# ============================================================================= +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "finmind.fullname" . }} + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- 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: + {{- if eq .service "backend" }} + name: {{ include "finmind.fullname" $ }}-backend + port: + number: {{ $.Values.backend.service.port }} + {{- else }} + name: {{ include "finmind.fullname" $ }}-frontend + port: + number: {{ $.Values.frontend.service.port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/namespace.yaml b/deploy/helm/finmind/templates/namespace.yaml new file mode 100644 index 00000000..24cfc290 --- /dev/null +++ b/deploy/helm/finmind/templates/namespace.yaml @@ -0,0 +1,9 @@ +# ============================================================================= +# FinMind — Namespace +# ============================================================================= +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} diff --git a/deploy/helm/finmind/templates/postgresql-deployment.yaml b/deploy/helm/finmind/templates/postgresql-deployment.yaml new file mode 100644 index 00000000..a737b585 --- /dev/null +++ b/deploy/helm/finmind/templates/postgresql-deployment.yaml @@ -0,0 +1,89 @@ +# ============================================================================= +# FinMind — PostgreSQL StatefulSet-style Deployment +# ============================================================================= +# For production, consider using a managed database (RDS, Cloud SQL, etc.) +# and set postgresql.enabled=false in values.yaml. +# ============================================================================= +{{- if .Values.postgresql.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-postgresql + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 4 }} +spec: + replicas: 1 + # Recreate strategy prevents data corruption from concurrent PG instances + strategy: + type: Recreate + selector: + matchLabels: + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 6 }} + template: + metadata: + labels: + {{- include "finmind.labels" . | nindent 8 }} + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 8 }} + spec: + serviceAccountName: {{ include "finmind.serviceAccountName" . }} + containers: + - name: postgresql + image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + ports: + - name: tcp-pg + containerPort: 5432 + protocol: TCP + 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: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + # Health probes using pg_isready + startupProbe: + exec: + command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + livenessProbe: + exec: + command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + volumes: + - name: data + {{- if .Values.postgresql.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "finmind.fullname" . }}-postgresql + {{- else }} + emptyDir: {} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/postgresql-pvc.yaml b/deploy/helm/finmind/templates/postgresql-pvc.yaml new file mode 100644 index 00000000..3b4369ad --- /dev/null +++ b/deploy/helm/finmind/templates/postgresql-pvc.yaml @@ -0,0 +1,22 @@ +# ============================================================================= +# FinMind — PostgreSQL Persistent Volume Claim +# ============================================================================= +{{- if and .Values.postgresql.enabled .Values.postgresql.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "finmind.fullname" . }}-postgresql + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.postgresql.persistence.accessModes | nindent 4 }} + {{- if .Values.postgresql.persistence.storageClass }} + storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgresql.persistence.size }} +{{- end }} diff --git a/deploy/helm/finmind/templates/postgresql-service.yaml b/deploy/helm/finmind/templates/postgresql-service.yaml new file mode 100644 index 00000000..6237d936 --- /dev/null +++ b/deploy/helm/finmind/templates/postgresql-service.yaml @@ -0,0 +1,22 @@ +# ============================================================================= +# FinMind — PostgreSQL Service +# ============================================================================= +{{- if .Values.postgresql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-postgresql + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: tcp-pg + protocol: TCP + name: tcp-pg + selector: + {{- include "finmind.selectorLabels" (dict "component" "postgresql" "context" .) | nindent 4 }} +{{- end }} diff --git a/deploy/helm/finmind/templates/redis-deployment.yaml b/deploy/helm/finmind/templates/redis-deployment.yaml new file mode 100644 index 00000000..4fd6e420 --- /dev/null +++ b/deploy/helm/finmind/templates/redis-deployment.yaml @@ -0,0 +1,81 @@ +# ============================================================================= +# FinMind — Redis Deployment +# ============================================================================= +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-redis + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 6 }} + template: + metadata: + labels: + {{- include "finmind.labels" . | nindent 8 }} + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 8 }} + spec: + serviceAccountName: {{ include "finmind.serviceAccountName" . }} + containers: + - name: redis + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" + command: + - redis-server + - --maxmemory + - {{ .Values.redis.config.maxmemory | quote }} + - --maxmemory-policy + - {{ .Values.redis.config.maxmemoryPolicy | quote }} + - --appendonly + - "yes" + - --save + - "60 1000" + ports: + - name: tcp-redis + containerPort: 6379 + protocol: TCP + volumeMounts: + - name: data + mountPath: /data + startupProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 3 + failureThreshold: 10 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 15 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 999 + allowPrivilegeEscalation: false + volumes: + - name: data + {{- if .Values.redis.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "finmind.fullname" . }}-redis + {{- else }} + emptyDir: {} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/redis-pvc.yaml b/deploy/helm/finmind/templates/redis-pvc.yaml new file mode 100644 index 00000000..aad38b91 --- /dev/null +++ b/deploy/helm/finmind/templates/redis-pvc.yaml @@ -0,0 +1,22 @@ +# ============================================================================= +# FinMind — Redis Persistent Volume Claim +# ============================================================================= +{{- if and .Values.redis.enabled .Values.redis.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "finmind.fullname" . }}-redis + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.redis.persistence.storageClass }} + storageClassName: {{ .Values.redis.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.redis.persistence.size }} +{{- end }} diff --git a/deploy/helm/finmind/templates/redis-service.yaml b/deploy/helm/finmind/templates/redis-service.yaml new file mode 100644 index 00000000..d5acc1e2 --- /dev/null +++ b/deploy/helm/finmind/templates/redis-service.yaml @@ -0,0 +1,22 @@ +# ============================================================================= +# FinMind — Redis Service +# ============================================================================= +{{- if .Values.redis.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-redis + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 6379 + targetPort: tcp-redis + protocol: TCP + name: tcp-redis + selector: + {{- include "finmind.selectorLabels" (dict "component" "redis" "context" .) | nindent 4 }} +{{- end }} diff --git a/deploy/helm/finmind/templates/secrets.yaml b/deploy/helm/finmind/templates/secrets.yaml new file mode 100644 index 00000000..902ae905 --- /dev/null +++ b/deploy/helm/finmind/templates/secrets.yaml @@ -0,0 +1,41 @@ +# ============================================================================= +# FinMind — Secrets +# ============================================================================= +# WARNING: In production, use an external secret manager (Vault, AWS Secrets +# Manager, GCP Secret Manager) or Sealed Secrets instead of this resource. +# This template exists for initial setup and development environments. +# ============================================================================= +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "finmind.fullname" . }}-secrets + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} +type: Opaque +stringData: + POSTGRES_USER: {{ .Values.secrets.POSTGRES_USER | quote }} + POSTGRES_PASSWORD: {{ required "secrets.POSTGRES_PASSWORD must be set" .Values.secrets.POSTGRES_PASSWORD | quote }} + POSTGRES_DB: {{ .Values.secrets.POSTGRES_DB | quote }} + JWT_SECRET: {{ required "secrets.JWT_SECRET must be set" .Values.secrets.JWT_SECRET | quote }} + {{- if .Values.secrets.GEMINI_API_KEY }} + GEMINI_API_KEY: {{ .Values.secrets.GEMINI_API_KEY | quote }} + {{- end }} + {{- if .Values.secrets.OPENAI_API_KEY }} + OPENAI_API_KEY: {{ .Values.secrets.OPENAI_API_KEY | quote }} + {{- end }} + {{- if .Values.secrets.TWILIO_ACCOUNT_SID }} + TWILIO_ACCOUNT_SID: {{ .Values.secrets.TWILIO_ACCOUNT_SID | quote }} + {{- end }} + {{- if .Values.secrets.TWILIO_AUTH_TOKEN }} + TWILIO_AUTH_TOKEN: {{ .Values.secrets.TWILIO_AUTH_TOKEN | quote }} + {{- end }} + {{- if .Values.secrets.TWILIO_WHATSAPP_FROM }} + TWILIO_WHATSAPP_FROM: {{ .Values.secrets.TWILIO_WHATSAPP_FROM | quote }} + {{- end }} + {{- if .Values.secrets.EMAIL_FROM }} + EMAIL_FROM: {{ .Values.secrets.EMAIL_FROM | quote }} + {{- end }} + {{- if .Values.secrets.SMTP_URL }} + SMTP_URL: {{ .Values.secrets.SMTP_URL | quote }} + {{- end }} diff --git a/deploy/helm/finmind/templates/serviceaccount.yaml b/deploy/helm/finmind/templates/serviceaccount.yaml new file mode 100644 index 00000000..a3266cf8 --- /dev/null +++ b/deploy/helm/finmind/templates/serviceaccount.yaml @@ -0,0 +1,16 @@ +# ============================================================================= +# FinMind — Service Account +# ============================================================================= +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "finmind.serviceAccountName" . }} + namespace: {{ .Values.namespace }} + labels: + {{- include "finmind.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/values.yaml b/deploy/helm/finmind/values.yaml new file mode 100644 index 00000000..fd853cfd --- /dev/null +++ b/deploy/helm/finmind/values.yaml @@ -0,0 +1,268 @@ +# ============================================================================= +# FinMind — Helm Chart Default Values +# ============================================================================= +# Override these via --set flags or a custom values-.yaml file. +# Secrets should be provided via --set or external secret managers, never +# committed to source control. +# ============================================================================= + +# --------------------------------------------------------------------------- +# Global settings +# --------------------------------------------------------------------------- +global: + # Container image registry prefix (e.g. ghcr.io/rohitdash08) + imageRegistry: "" + # Pull secrets for private registries + imagePullSecrets: [] + +# --------------------------------------------------------------------------- +# Backend (Flask + Gunicorn) +# --------------------------------------------------------------------------- +backend: + replicaCount: 2 + + image: + repository: ghcr.io/rohitdash08/finmind-backend + tag: latest + pullPolicy: IfNotPresent + + service: + type: ClusterIP + port: 8000 + + # Resource requests and limits tuned for production + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + + # Horizontal Pod Autoscaler + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 8 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + + # Environment variables from ConfigMap (non-sensitive) + config: + LOG_LEVEL: "INFO" + GEMINI_MODEL: "gemini-1.5-flash" + GUNICORN_WORKERS: "2" + GUNICORN_THREADS: "4" + + # Probes use the /health endpoint defined in the Flask app + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + startupProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + + # Pod disruption budget for zero-downtime deployments + podDisruptionBudget: + enabled: true + minAvailable: 1 + + # Rolling update strategy + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +# --------------------------------------------------------------------------- +# Frontend (React SPA on nginx) +# --------------------------------------------------------------------------- +frontend: + replicaCount: 2 + + image: + repository: ghcr.io/rohitdash08/finmind-frontend + tag: latest + pullPolicy: IfNotPresent + + service: + type: ClusterIP + port: 80 + + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: 500m + memory: 128Mi + + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 6 + targetCPUUtilizationPercentage: 70 + + livenessProbe: + httpGet: + path: /nginx-health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 15 + + readinessProbe: + httpGet: + path: /nginx-health + port: 80 + initialDelaySeconds: 3 + periodSeconds: 10 + + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +# --------------------------------------------------------------------------- +# PostgreSQL +# --------------------------------------------------------------------------- +postgresql: + # Set to false to use an external managed database + enabled: true + + image: + repository: postgres + tag: "16-alpine" + + auth: + # These are defaults — override in production via --set or secrets + username: finmind + password: "" # MUST be set at deploy time + database: finmind + + persistence: + enabled: true + size: 10Gi + storageClass: "" # Use cluster default + accessModes: + - ReadWriteOnce + + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + +# --------------------------------------------------------------------------- +# Redis +# --------------------------------------------------------------------------- +redis: + enabled: true + + image: + repository: redis + tag: "7-alpine" + + persistence: + enabled: true + size: 2Gi + storageClass: "" + + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + + config: + maxmemory: "128mb" + maxmemoryPolicy: "allkeys-lru" + +# --------------------------------------------------------------------------- +# Ingress +# --------------------------------------------------------------------------- +ingress: + enabled: true + className: nginx + annotations: + # cert-manager TLS automation + cert-manager.io/cluster-issuer: letsencrypt-prod + # Rate limiting + nginx.ingress.kubernetes.io/rate-limit: "100" + nginx.ingress.kubernetes.io/rate-limit-window: "1m" + # Proxy body size for file uploads + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + # CORS + nginx.ingress.kubernetes.io/enable-cors: "true" + hosts: + - host: finmind.example.com + paths: + - path: / + pathType: Prefix + service: frontend + - path: /api + pathType: Prefix + service: backend + - path: /health + pathType: Exact + service: backend + tls: + - secretName: finmind-tls + hosts: + - finmind.example.com + +# --------------------------------------------------------------------------- +# Secrets — provide at deploy time, never commit real values +# --------------------------------------------------------------------------- +secrets: + # Database + POSTGRES_USER: finmind + POSTGRES_PASSWORD: "" # --set secrets.POSTGRES_PASSWORD= + POSTGRES_DB: finmind + # Auth + JWT_SECRET: "" # --set secrets.JWT_SECRET= + # AI services (optional) + GEMINI_API_KEY: "" + OPENAI_API_KEY: "" + # Notifications (optional) + TWILIO_ACCOUNT_SID: "" + TWILIO_AUTH_TOKEN: "" + TWILIO_WHATSAPP_FROM: "" + EMAIL_FROM: "" + SMTP_URL: "" + +# --------------------------------------------------------------------------- +# Namespace +# --------------------------------------------------------------------------- +namespace: finmind + +# --------------------------------------------------------------------------- +# Service Account +# --------------------------------------------------------------------------- +serviceAccount: + create: true + name: finmind + annotations: {} diff --git a/deploy/platforms/aws/apprunner.yaml b/deploy/platforms/aws/apprunner.yaml new file mode 100644 index 00000000..eaa7ab15 --- /dev/null +++ b/deploy/platforms/aws/apprunner.yaml @@ -0,0 +1,51 @@ +# ============================================================================= +# FinMind — AWS App Runner Configuration +# ============================================================================= +# App Runner provides a simpler alternative to ECS Fargate. +# +# Setup: +# 1. Push image to ECR: +# aws ecr create-repository --repository-name finmind-backend +# docker build -f deploy/docker/backend.Dockerfile -t finmind-backend . +# docker tag finmind-backend:latest ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest +# docker push ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest +# 2. Create RDS PostgreSQL and ElastiCache Redis via AWS Console or CLI +# 3. Store secrets in AWS Secrets Manager +# 4. Deploy: +# aws apprunner create-service --cli-input-yaml file://deploy/platforms/aws/apprunner.yaml +# ============================================================================= + +ServiceName: finmind-backend +SourceConfiguration: + AuthenticationConfiguration: + AccessRoleArn: "arn:aws:iam::ACCOUNT_ID:role/AppRunnerECRAccessRole" + ImageRepository: + ImageIdentifier: "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest" + ImageRepositoryType: ECR + ImageConfiguration: + Port: "8000" + RuntimeEnvironmentVariables: + LOG_LEVEL: "INFO" + GEMINI_MODEL: "gemini-1.5-flash" + GUNICORN_WORKERS: "2" + GUNICORN_THREADS: "4" + RuntimeEnvironmentSecrets: + DATABASE_URL: "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/database-url" + REDIS_URL: "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/redis-url" + JWT_SECRET: "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/jwt-secret" + AutoDeploymentsEnabled: true +InstanceConfiguration: + Cpu: "1 vCPU" + Memory: "2 GB" +HealthCheckConfiguration: + Protocol: HTTP + Path: "/health" + Interval: 15 + Timeout: 5 + HealthyThreshold: 1 + UnhealthyThreshold: 3 +AutoScalingConfigurationArn: "arn:aws:apprunner:us-east-1:ACCOUNT_ID:autoscalingconfiguration/finmind-autoscale/1/latest" +NetworkConfiguration: + EgressConfiguration: + EgressType: VPC + VpcConnectorArn: "arn:aws:apprunner:us-east-1:ACCOUNT_ID:vpcconnector/finmind-vpc-connector/1/latest" diff --git a/deploy/platforms/aws/ecs-task-definition.json b/deploy/platforms/aws/ecs-task-definition.json new file mode 100644 index 00000000..48316ab6 --- /dev/null +++ b/deploy/platforms/aws/ecs-task-definition.json @@ -0,0 +1,58 @@ +{ + "_comment": "FinMind — AWS ECS Fargate Task Definition. See deploy/DEPLOYMENT.md for full setup instructions.", + "family": "finmind-backend", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole", + "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/finmindTaskRole", + "containerDefinitions": [ + { + "name": "finmind-backend", + "image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/finmind-backend:latest", + "essential": true, + "portMappings": [ + { + "containerPort": 8000, + "protocol": "tcp" + } + ], + "environment": [ + { "name": "GUNICORN_WORKERS", "value": "2" }, + { "name": "GUNICORN_THREADS", "value": "4" }, + { "name": "LOG_LEVEL", "value": "INFO" }, + { "name": "GEMINI_MODEL", "value": "gemini-1.5-flash" } + ], + "secrets": [ + { + "name": "DATABASE_URL", + "valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/database-url" + }, + { + "name": "REDIS_URL", + "valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/redis-url" + }, + { + "name": "JWT_SECRET", + "valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:finmind/jwt-secret" + } + ], + "healthCheck": { + "command": ["CMD-SHELL", "curl -sf http://localhost:8000/health || exit 1"], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 30 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/finmind-backend", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "ecs" + } + } + } + ] +} diff --git a/deploy/platforms/azure/containerapp.yaml b/deploy/platforms/azure/containerapp.yaml new file mode 100644 index 00000000..66e2c5b2 --- /dev/null +++ b/deploy/platforms/azure/containerapp.yaml @@ -0,0 +1,99 @@ +# ============================================================================= +# FinMind — Azure Container Apps Configuration +# ============================================================================= +# Azure Container Apps provides a serverless container platform. +# +# Setup: +# 1. Install Azure CLI and login: +# az login +# 2. Create resource group: +# az group create --name finmind-rg --location eastus +# 3. Create Container Apps environment: +# az containerapp env create --name finmind-env --resource-group finmind-rg --location eastus +# 4. Create Azure Database for PostgreSQL: +# az postgres flexible-server create --name finmind-db --resource-group finmind-rg \ +# --admin-user finmind --admin-password --sku-name Standard_B1ms --version 16 +# 5. Create Azure Cache for Redis: +# az redis create --name finmind-redis --resource-group finmind-rg --sku Basic --vm-size C0 +# 6. Build and push to ACR: +# az acr create --name finmindacr --resource-group finmind-rg --sku Basic +# az acr build --registry finmindacr --image finmind-backend:latest -f deploy/docker/backend.Dockerfile . +# 7. Deploy: +# az containerapp create --yaml deploy/platforms/azure/containerapp.yaml +# ============================================================================= + +name: finmind-backend +resourceGroup: finmind-rg +type: Microsoft.App/containerApps +location: eastus +properties: + managedEnvironmentId: /subscriptions/SUBSCRIPTION_ID/resourceGroups/finmind-rg/providers/Microsoft.App/managedEnvironments/finmind-env + configuration: + activeRevisionsMode: Multiple + ingress: + external: true + targetPort: 8000 + transport: http + traffic: + - latestRevision: true + weight: 100 + secrets: + - name: database-url + value: REPLACE_WITH_DATABASE_URL + - name: redis-url + value: REPLACE_WITH_REDIS_URL + - name: jwt-secret + value: REPLACE_WITH_JWT_SECRET + registries: + - server: finmindacr.azurecr.io + identity: system + template: + containers: + - name: finmind-backend + image: finmindacr.azurecr.io/finmind-backend:latest + resources: + cpu: 0.5 + memory: 1Gi + 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 + - name: GUNICORN_WORKERS + value: "2" + - name: GUNICORN_THREADS + value: "4" + probes: + - type: Startup + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + - type: Liveness + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 15 + - type: Readiness + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + scale: + minReplicas: 1 + maxReplicas: 10 + rules: + - name: http-scaling + http: + metadata: + concurrentRequests: "50" diff --git a/deploy/platforms/digitalocean/.do/app.yaml b/deploy/platforms/digitalocean/.do/app.yaml new file mode 100644 index 00000000..dbafae06 --- /dev/null +++ b/deploy/platforms/digitalocean/.do/app.yaml @@ -0,0 +1,108 @@ +# ============================================================================= +# FinMind — DigitalOcean App Platform Specification +# ============================================================================= +# DigitalOcean App Platform uses this file for Infrastructure as Code. +# +# Setup: +# 1. Install doctl: https://docs.digitalocean.com/reference/doctl/how-to/install/ +# 2. doctl auth init +# 3. Deploy: +# doctl apps create --spec deploy/platforms/digitalocean/.do/app.yaml +# 4. Set secrets in the DigitalOcean dashboard (JWT_SECRET, API keys) +# +# For Droplet deployment, see deploy/scripts/deploy.sh --platform digitalocean-droplet +# ============================================================================= + +name: finmind +region: nyc + +services: + # --------------------------------------------------------------------------- + # Backend API + # --------------------------------------------------------------------------- + - name: finmind-backend + dockerfile_path: deploy/docker/backend.Dockerfile + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + http_port: 8000 + instance_count: 1 + instance_size_slug: basic-xs + health_check: + http_path: /health + initial_delay_seconds: 15 + period_seconds: 15 + timeout_seconds: 5 + success_threshold: 1 + failure_threshold: 3 + envs: + - key: DATABASE_URL + scope: RUN_TIME + value: ${finmind-db.DATABASE_URL} + - key: REDIS_URL + scope: RUN_TIME + value: ${finmind-redis.REDIS_URL} + - key: JWT_SECRET + scope: RUN_TIME + type: SECRET + value: REPLACE_WITH_SECRET + - key: LOG_LEVEL + scope: RUN_TIME + value: INFO + - key: GEMINI_MODEL + scope: RUN_TIME + value: gemini-1.5-flash + - key: GUNICORN_WORKERS + scope: RUN_TIME + value: "2" + - key: GUNICORN_THREADS + scope: RUN_TIME + value: "4" + routes: + - path: /api + - path: /health + - path: /metrics + + # --------------------------------------------------------------------------- + # Frontend SPA + # --------------------------------------------------------------------------- + - name: finmind-frontend + environment_slug: node-js + github: + repo: rohitdash08/FinMind + branch: main + deploy_on_push: true + source_dir: app + build_command: npm ci && npm run build + http_port: 80 + instance_count: 1 + instance_size_slug: basic-xxs + dockerfile_path: deploy/docker/frontend.Dockerfile + envs: + - key: VITE_API_URL + scope: BUILD_TIME + value: ${APP_URL}/api + routes: + - path: / + +databases: + # --------------------------------------------------------------------------- + # PostgreSQL + # --------------------------------------------------------------------------- + - name: finmind-db + engine: PG + version: "16" + size: db-s-1vcpu-1gb + num_nodes: 1 + production: false + + # --------------------------------------------------------------------------- + # Redis + # --------------------------------------------------------------------------- + - name: finmind-redis + engine: REDIS + version: "7" + size: db-s-1vcpu-1gb + num_nodes: 1 + production: false diff --git a/deploy/platforms/fly/fly-frontend.toml b/deploy/platforms/fly/fly-frontend.toml new file mode 100644 index 00000000..549745b8 --- /dev/null +++ b/deploy/platforms/fly/fly-frontend.toml @@ -0,0 +1,37 @@ +# ============================================================================= +# FinMind — Fly.io Frontend Deployment +# ============================================================================= +# Deploy the frontend SPA as a separate Fly.io app. +# +# Usage: +# fly deploy --config deploy/platforms/fly/fly-frontend.toml +# ============================================================================= + +app = "finmind-frontend" +primary_region = "iad" +kill_signal = "SIGTERM" +kill_timeout = "10s" + +[build] + dockerfile = "deploy/docker/frontend.Dockerfile" + [build.args] + VITE_API_URL = "https://finmind-backend.fly.dev" + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = "stop" + auto_start_machines = true + min_machines_running = 1 + + [[http_service.checks]] + interval = "30s" + timeout = "3s" + grace_period = "10s" + method = "GET" + path = "/nginx-health" + +[[vm]] + memory = "256mb" + cpu_kind = "shared" + cpus = 1 diff --git a/deploy/platforms/fly/fly.toml b/deploy/platforms/fly/fly.toml new file mode 100644 index 00000000..59097a80 --- /dev/null +++ b/deploy/platforms/fly/fly.toml @@ -0,0 +1,73 @@ +# ============================================================================= +# FinMind — Fly.io Deployment Configuration +# ============================================================================= +# Fly.io uses this file to configure the application deployment. +# +# Setup: +# 1. Install flyctl: curl -L https://fly.io/install.sh | sh +# 2. fly auth login +# 3. fly apps create finmind-backend +# 4. Create a Postgres cluster: +# fly postgres create --name finmind-db +# fly postgres attach finmind-db --app finmind-backend +# 5. Create a Redis instance: +# fly redis create --name finmind-redis +# 6. Set secrets: +# fly secrets set JWT_SECRET="$(openssl rand -hex 32)" --app finmind-backend +# fly secrets set REDIS_URL="" --app finmind-backend +# 7. Deploy: +# fly deploy --config deploy/platforms/fly/fly.toml +# +# For the frontend, deploy separately: +# fly apps create finmind-frontend +# fly deploy --config deploy/platforms/fly/fly-frontend.toml +# ============================================================================= + +app = "finmind-backend" +primary_region = "iad" +kill_signal = "SIGTERM" +kill_timeout = "30s" + +[build] + dockerfile = "deploy/docker/backend.Dockerfile" + +[env] + LOG_LEVEL = "INFO" + GEMINI_MODEL = "gemini-1.5-flash" + GUNICORN_WORKERS = "2" + GUNICORN_THREADS = "4" + GUNICORN_BIND = "0.0.0.0:8080" + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "stop" + auto_start_machines = true + min_machines_running = 1 + processes = ["app"] + + [http_service.concurrency] + type = "requests" + hard_limit = 250 + soft_limit = 200 + + [[http_service.checks]] + interval = "15s" + timeout = "5s" + grace_period = "30s" + method = "GET" + path = "/health" + +[[vm]] + memory = "512mb" + cpu_kind = "shared" + cpus = 1 + +[metrics] + port = 8080 + path = "/metrics" + +[deploy] + strategy = "rolling" + release_command = "python -m flask --app wsgi:app init-db" diff --git a/deploy/platforms/gcp/app.yaml b/deploy/platforms/gcp/app.yaml new file mode 100644 index 00000000..d397df48 --- /dev/null +++ b/deploy/platforms/gcp/app.yaml @@ -0,0 +1,96 @@ +# ============================================================================= +# FinMind — Google Cloud Run Service Definition +# ============================================================================= +# This file defines the Cloud Run service configuration. +# +# Setup: +# 1. Install gcloud CLI and authenticate +# 2. Enable required APIs: +# gcloud services enable run.googleapis.com sqladmin.googleapis.com redis.googleapis.com +# 3. Create Cloud SQL PostgreSQL instance: +# gcloud sql instances create finmind-db --database-version=POSTGRES_16 \ +# --tier=db-f1-micro --region=us-central1 +# gcloud sql databases create finmind --instance=finmind-db +# gcloud sql users set-password postgres --instance=finmind-db --password= +# 4. Create Memorystore Redis: +# gcloud redis instances create finmind-redis --size=1 --region=us-central1 +# 5. Build and push: +# gcloud builds submit --tag gcr.io/$PROJECT_ID/finmind-backend \ +# --build-arg DOCKERFILE=deploy/docker/backend.Dockerfile +# 6. Deploy: +# gcloud run services replace deploy/platforms/gcp/app.yaml +# +# Environment variables containing secrets should be stored in +# Google Secret Manager and referenced via secretKeyRef. +# ============================================================================= + +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: finmind-backend + labels: + cloud.googleapis.com/location: us-central1 + annotations: + run.googleapis.com/ingress: all + run.googleapis.com/launch-stage: GA +spec: + template: + metadata: + annotations: + # Auto-scaling configuration + autoscaling.knative.dev/minScale: "1" + autoscaling.knative.dev/maxScale: "10" + # Cloud SQL connection + run.googleapis.com/cloudsql-instances: "PROJECT_ID:us-central1:finmind-db" + # CPU allocation: always allocated (not throttled between requests) + run.googleapis.com/cpu-throttling: "false" + # Startup CPU boost for faster cold starts + run.googleapis.com/startup-cpu-boost: "true" + spec: + containerConcurrency: 80 + timeoutSeconds: 300 + serviceAccountName: finmind-sa@PROJECT_ID.iam.gserviceaccount.com + containers: + - image: gcr.io/PROJECT_ID/finmind-backend:latest + ports: + - name: http1 + containerPort: 8080 + env: + - name: GUNICORN_BIND + value: "0.0.0.0:8080" + - name: LOG_LEVEL + value: "INFO" + - name: GEMINI_MODEL + value: "gemini-1.5-flash" + # Secrets from Secret Manager + - 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 + resources: + limits: + cpu: "1" + memory: 512Mi + startupProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + livenessProbe: + httpGet: + path: /health + port: 8080 + periodSeconds: 15 diff --git a/deploy/platforms/heroku/Procfile b/deploy/platforms/heroku/Procfile new file mode 100644 index 00000000..ea800a8d --- /dev/null +++ b/deploy/platforms/heroku/Procfile @@ -0,0 +1,29 @@ +# ============================================================================= +# FinMind — Heroku Procfile +# ============================================================================= +# Heroku uses this file to determine process types and start commands. +# +# Setup: +# 1. Install Heroku CLI: curl https://cli-assets.heroku.com/install.sh | sh +# 2. heroku login +# 3. heroku create finmind-backend +# 4. heroku addons:create heroku-postgresql:essential-0 +# 5. heroku addons:create heroku-redis:mini +# 6. Set config vars: +# heroku config:set JWT_SECRET="$(openssl rand -hex 32)" +# heroku config:set LOG_LEVEL=INFO +# heroku config:set GEMINI_MODEL=gemini-1.5-flash +# 7. Deploy: +# heroku container:push web --recursive +# heroku container:release web +# +# Alternatively, deploy the backend directly: +# heroku buildpacks:set heroku/python +# git subtree push --prefix packages/backend heroku main +# +# Note: Heroku automatically sets DATABASE_URL and REDIS_URL when you add +# the PostgreSQL and Redis addons. +# ============================================================================= + +web: sh -c 'cd packages/backend && python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind=0.0.0.0:$PORT --timeout=120 --graceful-timeout=30 wsgi:app' +release: cd packages/backend && python -m flask --app wsgi:app init-db diff --git a/deploy/platforms/netlify/netlify.toml b/deploy/platforms/netlify/netlify.toml new file mode 100644 index 00000000..0b9dde96 --- /dev/null +++ b/deploy/platforms/netlify/netlify.toml @@ -0,0 +1,53 @@ +# ============================================================================= +# FinMind — Netlify Configuration (Frontend Only) +# ============================================================================= +# Netlify is used to deploy the React SPA frontend. +# The backend must be deployed separately on another platform. +# +# Setup: +# 1. Install Netlify CLI: npm i -g netlify-cli +# 2. netlify login +# 3. netlify init (or connect via Netlify dashboard) +# 4. Set environment variable in Netlify dashboard: +# VITE_API_URL = https://your-backend-url.com +# 5. Deploy: +# netlify deploy --prod +# +# Netlify automatically provisions TLS and CDN. +# ============================================================================= + +[build] + base = "app" + command = "npm ci && npm run build" + publish = "dist" + +[build.environment] + NODE_VERSION = "20" + +# SPA redirect — all routes fall through to index.html +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +# Proxy API requests to the backend (avoids CORS in production) +[[redirects]] + from = "/api/*" + to = "https://your-finmind-backend.com/:splat" + status = 200 + force = true + +# Security headers +[[headers]] + for = "/*" + [headers.values] + X-Frame-Options = "SAMEORIGIN" + X-Content-Type-Options = "nosniff" + X-XSS-Protection = "1; mode=block" + Referrer-Policy = "strict-origin-when-cross-origin" + +# Cache hashed static assets aggressively +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" diff --git a/deploy/platforms/railway/railway.json b/deploy/platforms/railway/railway.json new file mode 100644 index 00000000..143c5620 --- /dev/null +++ b/deploy/platforms/railway/railway.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "deploy/docker/backend.Dockerfile" + }, + "deploy": { + "startCommand": "sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind=0.0.0.0:${PORT:-8000} --timeout=120 wsgi:app'", + "healthcheckPath": "/health", + "healthcheckTimeout": 30, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 5, + "numReplicas": 1 + } +} diff --git a/deploy/platforms/railway/railway.toml b/deploy/platforms/railway/railway.toml new file mode 100644 index 00000000..15cf8612 --- /dev/null +++ b/deploy/platforms/railway/railway.toml @@ -0,0 +1,29 @@ +# ============================================================================= +# FinMind — Railway Deployment Configuration +# ============================================================================= +# Railway auto-detects services from this file. +# +# Setup: +# 1. Install Railway CLI: npm i -g @railway/cli +# 2. railway login +# 3. railway init +# 4. Add PostgreSQL and Redis plugins in the Railway dashboard +# 5. Set environment variables (see .env.example) +# 6. railway up +# +# Railway automatically provisions DATABASE_URL and REDIS_URL when you add +# the PostgreSQL and Redis plugins — no manual config needed for those. +# ============================================================================= + +[build] +builder = "DOCKERFILE" +dockerfilePath = "deploy/docker/backend.Dockerfile" +watchPatterns = ["packages/backend/**"] + +[deploy] +startCommand = "sh -c 'python -m flask --app wsgi:app init-db && gunicorn --workers=2 --threads=4 --bind=0.0.0.0:${PORT:-8000} --timeout=120 wsgi:app'" +healthcheckPath = "/health" +healthcheckTimeout = 30 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 5 +numReplicas = 1 diff --git a/deploy/platforms/render/render.yaml b/deploy/platforms/render/render.yaml new file mode 100644 index 00000000..f27e32d8 --- /dev/null +++ b/deploy/platforms/render/render.yaml @@ -0,0 +1,102 @@ +# ============================================================================= +# FinMind — Render Blueprint (render.yaml) +# ============================================================================= +# Render uses this file for Infrastructure as Code deployments. +# +# Setup: +# 1. Push this repo to GitHub +# 2. Go to https://dashboard.render.com/blueprints +# 3. Click "New Blueprint Instance" and connect your repo +# 4. Render will auto-detect this file and create all services +# 5. Set secret environment variables in the Render dashboard +# +# Render automatically manages TLS certificates and provides a .onrender.com +# domain for each web service. +# ============================================================================= + +services: + # --------------------------------------------------------------------------- + # Backend API + # --------------------------------------------------------------------------- + - type: web + name: finmind-backend + runtime: docker + dockerfilePath: deploy/docker/backend.Dockerfile + dockerContext: . + plan: starter + region: oregon + healthCheckPath: /health + envVars: + - key: DATABASE_URL + fromDatabase: + name: finmind-db + property: connectionString + - key: REDIS_URL + fromService: + name: finmind-redis + type: redis + property: connectionString + - key: JWT_SECRET + generateValue: true + - key: LOG_LEVEL + value: INFO + - key: GEMINI_MODEL + value: gemini-1.5-flash + - key: GEMINI_API_KEY + sync: false # Set manually in dashboard + - key: GUNICORN_WORKERS + value: "2" + - key: GUNICORN_THREADS + value: "4" + scaling: + minInstances: 1 + maxInstances: 3 + targetMemoryPercent: 80 + targetCPUPercent: 70 + + # --------------------------------------------------------------------------- + # Frontend SPA + # --------------------------------------------------------------------------- + - type: web + name: finmind-frontend + runtime: static + buildCommand: cd app && npm ci && npm run build + staticPublishPath: app/dist + headers: + - path: /* + name: X-Frame-Options + value: SAMEORIGIN + - path: /* + name: X-Content-Type-Options + value: nosniff + routes: + - type: rewrite + source: /* + destination: /index.html + envVars: + - key: VITE_API_URL + fromService: + name: finmind-backend + type: web + property: url + + # --------------------------------------------------------------------------- + # Redis + # --------------------------------------------------------------------------- + - type: redis + name: finmind-redis + plan: starter + ipAllowList: [] # Allow only internal access + maxmemoryPolicy: allkeys-lru + +databases: + # --------------------------------------------------------------------------- + # PostgreSQL + # --------------------------------------------------------------------------- + - name: finmind-db + plan: starter + databaseName: finmind + user: finmind + region: oregon + postgresMajorVersion: "16" + ipAllowList: [] # Allow only internal access diff --git a/deploy/platforms/vercel/vercel.json b/deploy/platforms/vercel/vercel.json new file mode 100644 index 00000000..2c873cc4 --- /dev/null +++ b/deploy/platforms/vercel/vercel.json @@ -0,0 +1,37 @@ +{ + "_comment": "FinMind — Vercel Configuration (Frontend Only). Set VITE_API_URL in Vercel dashboard.", + "version": 2, + "framework": "vite", + "buildCommand": "npm ci && npm run build", + "outputDirectory": "dist", + "installCommand": "npm ci", + "devCommand": "npm run dev", + "rootDirectory": "app", + "rewrites": [ + { + "source": "/api/:path*", + "destination": "https://your-finmind-backend.com/:path*" + }, + { + "source": "/(.*)", + "destination": "/index.html" + } + ], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Frame-Options", "value": "SAMEORIGIN" }, + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-XSS-Protection", "value": "1; mode=block" }, + { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" } + ] + }, + { + "source": "/assets/(.*)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ] +} diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh new file mode 100644 index 00000000..524cd6f7 --- /dev/null +++ b/deploy/scripts/deploy.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +# ============================================================================= +# FinMind — Unified Deployment Script +# ============================================================================= +# A single entry point for deploying FinMind to any supported platform. +# +# Usage: +# ./deploy/scripts/deploy.sh --platform [options] +# +# Platforms: +# docker Docker Compose (production) +# kubernetes Kubernetes via Helm +# railway Railway +# render Render +# fly Fly.io +# heroku Heroku +# digitalocean DigitalOcean App Platform +# aws-ecs AWS ECS Fargate +# aws-apprunner AWS App Runner +# gcp-cloudrun GCP Cloud Run +# azure Azure Container Apps +# netlify Netlify (frontend only) +# vercel Vercel (frontend only) +# +# Options: +# --env Path to .env file (default: .env) +# --tag Docker image tag (default: latest) +# --dry-run Print commands without executing +# --help Show this help message +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DEPLOY_DIR="${REPO_ROOT}/deploy" + +# --------------------------------------------------------------------------- +# Defaults +# --------------------------------------------------------------------------- +PLATFORM="" +ENV_FILE="${REPO_ROOT}/.env" +IMAGE_TAG="latest" +DRY_RUN=false + +# --------------------------------------------------------------------------- +# Colors for output +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +run_cmd() { + if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}[DRY-RUN]${NC} $*" + else + log_info "Running: $*" + eval "$@" + fi +} + +check_command() { + if ! command -v "$1" &>/dev/null; then + log_error "'$1' is not installed. Please install it first." + exit 1 + fi +} + +usage() { + head -35 "$0" | tail -30 + exit 0 +} + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --env) ENV_FILE="$2"; shift 2 ;; + --tag) IMAGE_TAG="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --help|-h) usage ;; + *) log_error "Unknown option: $1"; usage ;; + esac +done + +if [ -z "$PLATFORM" ]; then + log_error "Platform is required. Use --platform " + usage +fi + +# --------------------------------------------------------------------------- +# Load environment file if it exists +# --------------------------------------------------------------------------- +if [ -f "$ENV_FILE" ]; then + log_info "Loading environment from ${ENV_FILE}" + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +else + log_warn "No .env file found at ${ENV_FILE}. Using environment variables." +fi + +# --------------------------------------------------------------------------- +# Platform: Docker Compose +# --------------------------------------------------------------------------- +deploy_docker() { + log_info "Deploying FinMind with Docker Compose (production)" + check_command docker + + run_cmd "docker compose -f ${DEPLOY_DIR}/docker/docker-compose.prod.yml \ + --env-file ${ENV_FILE} \ + build --no-cache" + + run_cmd "docker compose -f ${DEPLOY_DIR}/docker/docker-compose.prod.yml \ + --env-file ${ENV_FILE} \ + up -d" + + log_ok "Docker Compose deployment complete." + log_info "Frontend: http://localhost:${LISTEN_PORT:-80}" + log_info "Health: http://localhost:${LISTEN_PORT:-80}/health" +} + +# --------------------------------------------------------------------------- +# Platform: Kubernetes (Helm) +# --------------------------------------------------------------------------- +deploy_kubernetes() { + log_info "Deploying FinMind to Kubernetes via Helm" + check_command helm + check_command kubectl + + local HELM_ARGS=( + "upgrade" "--install" "finmind" + "${DEPLOY_DIR}/helm/finmind" + "--namespace" "finmind" + "--create-namespace" + "--set" "backend.image.tag=${IMAGE_TAG}" + "--set" "frontend.image.tag=${IMAGE_TAG}" + ) + + # Pass secrets via --set if environment variables are available + [ -n "${POSTGRES_PASSWORD:-}" ] && HELM_ARGS+=("--set" "secrets.POSTGRES_PASSWORD=${POSTGRES_PASSWORD}") + [ -n "${JWT_SECRET:-}" ] && HELM_ARGS+=("--set" "secrets.JWT_SECRET=${JWT_SECRET}") + [ -n "${GEMINI_API_KEY:-}" ] && HELM_ARGS+=("--set" "secrets.GEMINI_API_KEY=${GEMINI_API_KEY}") + + run_cmd "helm ${HELM_ARGS[*]}" + log_ok "Helm deployment complete. Run 'kubectl get pods -n finmind' to check status." +} + +# --------------------------------------------------------------------------- +# Platform: Railway +# --------------------------------------------------------------------------- +deploy_railway() { + log_info "Deploying FinMind to Railway" + check_command railway + + run_cmd "cd ${REPO_ROOT} && railway up --config ${DEPLOY_DIR}/platforms/railway/railway.toml" + log_ok "Railway deployment triggered." +} + +# --------------------------------------------------------------------------- +# Platform: Render +# --------------------------------------------------------------------------- +deploy_render() { + log_info "Deploying FinMind to Render" + log_info "Render deployments are triggered via the dashboard blueprint." + log_info "Blueprint file: ${DEPLOY_DIR}/platforms/render/render.yaml" + log_info "" + log_info "Steps:" + log_info " 1. Push this repo to GitHub" + log_info " 2. Go to https://dashboard.render.com/blueprints" + log_info " 3. Click 'New Blueprint Instance'" + log_info " 4. Connect your GitHub repo" + log_info " 5. Render will auto-detect render.yaml" + log_ok "Render config ready." +} + +# --------------------------------------------------------------------------- +# Platform: Fly.io +# --------------------------------------------------------------------------- +deploy_fly() { + log_info "Deploying FinMind to Fly.io" + check_command fly + + # Backend + log_info "Deploying backend..." + run_cmd "cd ${REPO_ROOT} && fly deploy --config ${DEPLOY_DIR}/platforms/fly/fly.toml" + + # Frontend + log_info "Deploying frontend..." + run_cmd "cd ${REPO_ROOT} && fly deploy --config ${DEPLOY_DIR}/platforms/fly/fly-frontend.toml" + + log_ok "Fly.io deployment complete." +} + +# --------------------------------------------------------------------------- +# Platform: Heroku +# --------------------------------------------------------------------------- +deploy_heroku() { + log_info "Deploying FinMind to Heroku" + check_command heroku + + log_info "Deploying backend via container..." + run_cmd "cd ${REPO_ROOT} && heroku container:push web \ + --app finmind-backend \ + --dockerfile ${DEPLOY_DIR}/docker/backend.Dockerfile" + run_cmd "heroku container:release web --app finmind-backend" + + log_ok "Heroku deployment complete." +} + +# --------------------------------------------------------------------------- +# Platform: DigitalOcean App Platform +# --------------------------------------------------------------------------- +deploy_digitalocean() { + log_info "Deploying FinMind to DigitalOcean App Platform" + check_command doctl + + run_cmd "doctl apps create --spec ${DEPLOY_DIR}/platforms/digitalocean/.do/app.yaml" + log_ok "DigitalOcean deployment triggered." +} + +# --------------------------------------------------------------------------- +# Platform: AWS ECS Fargate +# --------------------------------------------------------------------------- +deploy_aws_ecs() { + log_info "Deploying FinMind to AWS ECS Fargate" + check_command aws + check_command docker + + local ACCOUNT_ID + ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + local ECR_REPO="${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/finmind-backend" + + log_info "Building and pushing Docker image to ECR..." + run_cmd "aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${ECR_REPO}" + run_cmd "docker build -f ${DEPLOY_DIR}/docker/backend.Dockerfile -t finmind-backend:${IMAGE_TAG} ${REPO_ROOT}" + run_cmd "docker tag finmind-backend:${IMAGE_TAG} ${ECR_REPO}:${IMAGE_TAG}" + run_cmd "docker push ${ECR_REPO}:${IMAGE_TAG}" + + log_info "Updating ECS service..." + run_cmd "aws ecs update-service --cluster finmind --service finmind-backend --force-new-deployment" + log_ok "AWS ECS deployment triggered." +} + +# --------------------------------------------------------------------------- +# Platform: AWS App Runner +# --------------------------------------------------------------------------- +deploy_aws_apprunner() { + log_info "Deploying FinMind to AWS App Runner" + check_command aws + + log_info "See ${DEPLOY_DIR}/platforms/aws/apprunner.yaml for configuration." + log_info "Use: aws apprunner create-service --cli-input-yaml file://${DEPLOY_DIR}/platforms/aws/apprunner.yaml" + log_ok "AWS App Runner config ready." +} + +# --------------------------------------------------------------------------- +# Platform: GCP Cloud Run +# --------------------------------------------------------------------------- +deploy_gcp_cloudrun() { + log_info "Deploying FinMind to Google Cloud Run" + check_command gcloud + + local PROJECT_ID + PROJECT_ID=$(gcloud config get-value project 2>/dev/null) + + log_info "Building image with Cloud Build..." + run_cmd "cd ${REPO_ROOT} && gcloud builds submit \ + --tag gcr.io/${PROJECT_ID}/finmind-backend:${IMAGE_TAG} \ + --timeout=600 ." + + log_info "Deploying to Cloud Run..." + run_cmd "gcloud run deploy finmind-backend \ + --image gcr.io/${PROJECT_ID}/finmind-backend:${IMAGE_TAG} \ + --region us-central1 \ + --platform managed \ + --port 8000 \ + --memory 512Mi \ + --cpu 1 \ + --min-instances 1 \ + --max-instances 10 \ + --allow-unauthenticated" + + log_ok "GCP Cloud Run deployment complete." +} + +# --------------------------------------------------------------------------- +# Platform: Azure Container Apps +# --------------------------------------------------------------------------- +deploy_azure() { + log_info "Deploying FinMind to Azure Container Apps" + check_command az + + log_info "See ${DEPLOY_DIR}/platforms/azure/containerapp.yaml for configuration." + run_cmd "az containerapp update \ + --name finmind-backend \ + --resource-group finmind-rg \ + --yaml ${DEPLOY_DIR}/platforms/azure/containerapp.yaml" + + log_ok "Azure Container Apps deployment complete." +} + +# --------------------------------------------------------------------------- +# Platform: Netlify (frontend only) +# --------------------------------------------------------------------------- +deploy_netlify() { + log_info "Deploying FinMind frontend to Netlify" + check_command netlify + + run_cmd "cd ${REPO_ROOT}/app && npm ci && npm run build" + run_cmd "netlify deploy --prod --dir=${REPO_ROOT}/app/dist" + log_ok "Netlify deployment complete." +} + +# --------------------------------------------------------------------------- +# Platform: Vercel (frontend only) +# --------------------------------------------------------------------------- +deploy_vercel() { + log_info "Deploying FinMind frontend to Vercel" + check_command vercel + + run_cmd "cd ${REPO_ROOT}/app && vercel --prod" + log_ok "Vercel deployment complete." +} + +# --------------------------------------------------------------------------- +# Dispatch to platform handler +# --------------------------------------------------------------------------- +case "$PLATFORM" in + docker) deploy_docker ;; + kubernetes|k8s) deploy_kubernetes ;; + railway) deploy_railway ;; + render) deploy_render ;; + fly) deploy_fly ;; + heroku) deploy_heroku ;; + digitalocean) deploy_digitalocean ;; + aws-ecs) deploy_aws_ecs ;; + aws-apprunner) deploy_aws_apprunner ;; + gcp-cloudrun) deploy_gcp_cloudrun ;; + azure) deploy_azure ;; + netlify) deploy_netlify ;; + vercel) deploy_vercel ;; + *) + log_error "Unknown platform: ${PLATFORM}" + usage + ;; +esac diff --git a/deploy/scripts/setup-local.sh b/deploy/scripts/setup-local.sh new file mode 100644 index 00000000..5a5d1ca0 --- /dev/null +++ b/deploy/scripts/setup-local.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# ============================================================================= +# FinMind — Local Development Environment Setup +# ============================================================================= +# Sets up everything needed to run FinMind locally for development. +# +# Usage: +# ./deploy/scripts/setup-local.sh [options] +# +# Options: +# --docker Use Docker Compose for local dev (default) +# --tilt Use Tilt + local Kubernetes for local dev +# --bare Set up without containers (Python + Node directly) +# --skip-deps Skip dependency installation checks +# --help Show this help message +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Defaults +MODE="docker" +SKIP_DEPS=false + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +usage() { + head -17 "$0" | tail -13 + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --docker) MODE="docker"; shift ;; + --tilt) MODE="tilt"; shift ;; + --bare) MODE="bare"; shift ;; + --skip-deps) SKIP_DEPS=true; shift ;; + --help|-h) usage ;; + *) log_error "Unknown option: $1"; usage ;; + esac +done + +# --------------------------------------------------------------------------- +# Step 1: Create .env from example if it does not exist +# --------------------------------------------------------------------------- +setup_env() { + if [ ! -f "${REPO_ROOT}/.env" ]; then + log_info "Creating .env from .env.example..." + cp "${REPO_ROOT}/.env.example" "${REPO_ROOT}/.env" + + # Generate a random JWT secret for local development + if command -v openssl &>/dev/null; then + local jwt_secret + jwt_secret=$(openssl rand -hex 32) + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/JWT_SECRET=\"change-me\"/JWT_SECRET=\"${jwt_secret}\"/" "${REPO_ROOT}/.env" + else + sed -i "s/JWT_SECRET=\"change-me\"/JWT_SECRET=\"${jwt_secret}\"/" "${REPO_ROOT}/.env" + fi + log_ok "Generated random JWT_SECRET" + else + log_warn "openssl not found — please set JWT_SECRET in .env manually" + fi + + log_ok ".env file created. Edit it to add API keys if needed." + else + log_ok ".env file already exists." + fi +} + +# --------------------------------------------------------------------------- +# Step 2: Check system dependencies +# --------------------------------------------------------------------------- +check_dependencies() { + if [ "$SKIP_DEPS" = true ]; then + log_warn "Skipping dependency checks (--skip-deps)" + return + fi + + log_info "Checking system dependencies..." + + local missing=() + + case "$MODE" in + docker) + command -v docker &>/dev/null || missing+=("docker") + ;; + tilt) + command -v docker &>/dev/null || missing+=("docker") + command -v kubectl &>/dev/null || missing+=("kubectl") + command -v tilt &>/dev/null || missing+=("tilt") + ;; + bare) + command -v python3 &>/dev/null || missing+=("python3") + command -v node &>/dev/null || missing+=("node (v20+)") + command -v npm &>/dev/null || missing+=("npm") + ;; + esac + + if [ ${#missing[@]} -gt 0 ]; then + log_error "Missing dependencies: ${missing[*]}" + log_error "Please install them and try again." + exit 1 + fi + + log_ok "All dependencies present." +} + +# --------------------------------------------------------------------------- +# Step 3: Start services +# --------------------------------------------------------------------------- +start_docker() { + log_info "Starting FinMind with Docker Compose (development mode)..." + + # Use the root docker-compose.yml which includes dev tools and hot-reload + cd "${REPO_ROOT}" + docker compose up -d --build + + log_ok "Development environment is running." + echo "" + log_info "Services:" + log_info " Frontend: http://localhost:5173" + log_info " Backend API: http://localhost:8000" + log_info " Health: http://localhost:8000/health" + log_info " Grafana: http://localhost:3000" + log_info " Prometheus: http://localhost:9090" + echo "" + log_info "Useful commands:" + log_info " docker compose logs -f backend # Backend logs" + log_info " docker compose logs -f frontend-dev # Frontend logs" + log_info " docker compose down # Stop everything" +} + +start_tilt() { + log_info "Starting FinMind with Tilt (local Kubernetes)..." + + # Verify a Kubernetes cluster is running + if ! kubectl cluster-info &>/dev/null; then + log_error "No Kubernetes cluster found. Start Docker Desktop K8s, minikube, or kind." + exit 1 + fi + + cd "${REPO_ROOT}/deploy/tilt" + tilt up + + log_ok "Tilt is running. Open http://localhost:10350 for the Tilt dashboard." +} + +start_bare() { + log_info "Setting up bare-metal development environment..." + + # Backend + log_info "Setting up Python backend..." + cd "${REPO_ROOT}/packages/backend" + + if [ ! -d "venv" ]; then + python3 -m venv venv + log_ok "Created Python virtual environment" + fi + + # shellcheck disable=SC1091 + source venv/bin/activate + pip install -r requirements.txt + log_ok "Backend dependencies installed" + + # Frontend + log_info "Setting up Node.js frontend..." + cd "${REPO_ROOT}/app" + npm ci + log_ok "Frontend dependencies installed" + + echo "" + log_info "Local setup complete. Start the services:" + echo "" + log_info " Terminal 1 (PostgreSQL + Redis via Docker):" + log_info " docker compose up postgres redis" + echo "" + log_info " Terminal 2 (Backend):" + log_info " cd packages/backend && source venv/bin/activate" + log_info " flask --app wsgi:app init-db" + log_info " flask --app wsgi:app run --port 8000 --reload" + echo "" + log_info " Terminal 3 (Frontend):" + log_info " cd app && npm run dev" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +log_info "FinMind Local Development Setup (mode: ${MODE})" +echo "" + +setup_env +check_dependencies + +case "$MODE" in + docker) start_docker ;; + tilt) start_tilt ;; + bare) start_bare ;; +esac diff --git a/deploy/tilt/Tiltfile b/deploy/tilt/Tiltfile new file mode 100644 index 00000000..7154bc40 --- /dev/null +++ b/deploy/tilt/Tiltfile @@ -0,0 +1,330 @@ +# ============================================================================= +# FinMind — Tiltfile for Local Kubernetes Development +# ============================================================================= +# +# Prerequisites: +# - Docker Desktop with Kubernetes enabled, or minikube/kind/k3d +# - Tilt installed (https://docs.tilt.dev/install.html) +# - kubectl configured to point at your local cluster +# +# Usage: +# cd deploy/tilt +# tilt up +# +# This Tiltfile provides: +# - Live-reload for backend Python code (no image rebuild needed) +# - Live-reload for frontend source (Vite HMR via port-forward) +# - Automatic image builds when Dockerfiles change +# - Port forwarding for all services +# - Resource grouping in the Tilt UI +# ============================================================================= + +# Load the Kubernetes YAML generated by Helm +load('ext://helm_resource', 'helm_resource', 'helm_repo') +load('ext://restart_process', 'docker_build_with_restart') + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +# Root of the FinMind repository (two levels up from deploy/tilt/) +REPO_ROOT = '../..' + +# Allow the local k8s context +allow_k8s_contexts([ + 'docker-desktop', + 'minikube', + 'kind-kind', + 'k3d-k3s-default', + 'rancher-desktop', +]) + +# --------------------------------------------------------------------------- +# Namespace +# --------------------------------------------------------------------------- +k8s_yaml(REPO_ROOT + '/deploy/k8s/namespace.yaml') + +# --------------------------------------------------------------------------- +# Infrastructure: PostgreSQL + Redis (use existing K8s manifests as base) +# --------------------------------------------------------------------------- +# PostgreSQL +k8s_yaml(blob(""" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: finmind +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: finmind + - name: POSTGRES_PASSWORD + value: finmind-dev + - name: POSTGRES_DB + value: finmind + readinessProbe: + exec: + command: ["pg_isready", "-U", "finmind"] + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: finmind +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +""")) + +k8s_resource('postgres', + port_forwards=['5432:5432'], + labels=['infrastructure'], +) + +# Redis +k8s_yaml(blob(""" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: finmind +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 3 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: finmind +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 +""")) + +k8s_resource('redis', + port_forwards=['6379:6379'], + labels=['infrastructure'], +) + +# --------------------------------------------------------------------------- +# Backend — build image with live-reload of Python files +# --------------------------------------------------------------------------- +docker_build( + 'finmind-backend', + context=REPO_ROOT, + dockerfile=REPO_ROOT + '/deploy/docker/backend.Dockerfile', + live_update=[ + # Sync Python source files into the running container + sync(REPO_ROOT + '/packages/backend/app', '/app/app'), + sync(REPO_ROOT + '/packages/backend/wsgi.py', '/app/wsgi.py'), + # Restart gunicorn when Python files change + run('kill -HUP 1 || true', trigger=[ + REPO_ROOT + '/packages/backend/app', + REPO_ROOT + '/packages/backend/wsgi.py', + ]), + ], + ignore=[ + '**/__pycache__', + '**/*.pyc', + '**/tests', + '**/.pytest_cache', + ], +) + +k8s_yaml(blob(""" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: finmind +spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: finmind-backend + ports: + - containerPort: 8000 + env: + - name: DATABASE_URL + value: "postgresql+psycopg2://finmind:finmind-dev@postgres:5432/finmind" + - name: REDIS_URL + value: "redis://redis:6379/0" + - name: JWT_SECRET + value: "tilt-dev-secret-not-for-production" + - name: LOG_LEVEL + value: "DEBUG" + - name: GEMINI_MODEL + value: "gemini-1.5-flash" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 15 +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: finmind +spec: + selector: + app: backend + ports: + - port: 8000 + targetPort: 8000 +""")) + +k8s_resource('backend', + port_forwards=['8000:8000'], + resource_deps=['postgres', 'redis'], + labels=['application'], +) + +# --------------------------------------------------------------------------- +# Frontend — Vite dev server with HMR for instant feedback +# --------------------------------------------------------------------------- +# In development mode, run the Vite dev server instead of building + nginx +docker_build( + 'finmind-frontend-dev', + context=REPO_ROOT + '/app', + dockerfile_contents=""" +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] +""", + live_update=[ + # Sync source files for Vite HMR + sync(REPO_ROOT + '/app/src', '/app/src'), + sync(REPO_ROOT + '/app/public', '/app/public'), + sync(REPO_ROOT + '/app/index.html', '/app/index.html'), + # Only reinstall deps if package.json changes + fall_back_on([ + REPO_ROOT + '/app/package.json', + REPO_ROOT + '/app/package-lock.json', + ]), + ], + ignore=[ + 'node_modules', + 'dist', + '**/*.test.*', + ], +) + +k8s_yaml(blob(""" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: finmind +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: finmind-frontend-dev + ports: + - containerPort: 5173 + env: + - name: VITE_API_URL + value: "http://localhost:8000" + - name: CHOKIDAR_USEPOLLING + value: "true" + readinessProbe: + httpGet: + path: / + port: 5173 + initialDelaySeconds: 15 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: finmind +spec: + selector: + app: frontend + ports: + - port: 5173 + targetPort: 5173 +""")) + +k8s_resource('frontend', + port_forwards=['5173:5173'], + resource_deps=['backend'], + labels=['application'], +) + +# --------------------------------------------------------------------------- +# Tilt UI configuration +# --------------------------------------------------------------------------- +# Update settings: check more frequently for faster feedback +update_settings( + max_parallel_updates=3, + k8s_upsert_timeout_secs=120, +)