From 4e7e25f074f7a39c02d3040ba645c7d9bb1990af Mon Sep 17 00:00:00 2001 From: daatsuka Date: Mon, 23 Mar 2026 03:55:39 +0900 Subject: [PATCH] feat: implement universal one-click deployment with Docker & Docker-compose (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tiltfile: local Kubernetes dev workflow with live_update for fast iteration - deploy/helm/finmind/: full Helm chart with HPA, Ingress/TLS, backend deployment - deploy/platforms/railway.json: Railway one-click config - deploy/platforms/heroku.yml: Heroku Container Registry config - deploy/platforms/render.yaml: Render services + managed DB + Redis - deploy/platforms/fly.toml: Fly.io config (Tokyo region, health checks) - deploy/platforms/do-app.yaml: DigitalOcean App Platform spec - deploy/one-click.sh: universal script — ./deploy/one-click.sh [platform] supports: docker | k8s | helm | tilt | railway | render | fly | heroku | do Closes #144 --- Tiltfile | 47 +++++++++++ deploy/helm/finmind/Chart.yaml | 12 +++ deploy/helm/finmind/templates/_helpers.tpl | 18 ++++ .../finmind/templates/deployment-backend.yaml | 50 +++++++++++ deploy/helm/finmind/templates/hpa.yaml | 20 +++++ deploy/helm/finmind/templates/ingress.yaml | 29 +++++++ deploy/helm/finmind/values.yaml | 70 ++++++++++++++++ deploy/one-click.sh | 84 +++++++++++++++++++ deploy/platforms/do-app.yaml | 40 +++++++++ deploy/platforms/fly.toml | 35 ++++++++ deploy/platforms/heroku.yml | 10 +++ deploy/platforms/railway.json | 14 ++++ deploy/platforms/render.yaml | 39 +++++++++ 13 files changed, 468 insertions(+) create mode 100644 Tiltfile create mode 100644 deploy/helm/finmind/Chart.yaml create mode 100644 deploy/helm/finmind/templates/_helpers.tpl create mode 100644 deploy/helm/finmind/templates/deployment-backend.yaml create mode 100644 deploy/helm/finmind/templates/hpa.yaml create mode 100644 deploy/helm/finmind/templates/ingress.yaml create mode 100644 deploy/helm/finmind/values.yaml create mode 100755 deploy/one-click.sh create mode 100644 deploy/platforms/do-app.yaml create mode 100644 deploy/platforms/fly.toml create mode 100644 deploy/platforms/heroku.yml create mode 100644 deploy/platforms/railway.json create mode 100644 deploy/platforms/render.yaml diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 00000000..86a66eaf --- /dev/null +++ b/Tiltfile @@ -0,0 +1,47 @@ +# Tiltfile — FinMind local Kubernetes dev workflow +# Usage: tilt up +# Prerequisites: Docker, kubectl, a local K8s cluster (kind / minikube / Docker Desktop) + +load('ext://helm_resource', 'helm_resource', 'helm_repo') +load('ext://restart_process', 'docker_build_with_restart') + +# ── Build images ────────────────────────────────────────────────────────────── +docker_build( + 'finmind-backend', + context='./packages/backend', + dockerfile='./packages/backend/Dockerfile', + live_update=[ + sync('./packages/backend', '/app'), + run('pip install -r /app/requirements.txt', trigger='./packages/backend/requirements.txt'), + ], +) + +docker_build( + 'finmind-frontend', + context='./app', + dockerfile='./app/Dockerfile', + live_update=[ + sync('./app/src', '/app/src'), + ], +) + +# ── Apply K8s manifests ─────────────────────────────────────────────────────── +k8s_yaml([ + 'deploy/k8s/namespace.yaml', + 'deploy/k8s/app-stack.yaml', + 'deploy/k8s/monitoring-stack.yaml', +]) + +# ── Resource grouping ───────────────────────────────────────────────────────── +k8s_resource('finmind-backend', port_forwards='8000:8000', labels=['backend']) +k8s_resource('finmind-frontend', port_forwards='3000:80', labels=['frontend']) +k8s_resource('postgres', port_forwards='5432:5432', labels=['data']) +k8s_resource('redis', port_forwards='6379:6379', labels=['data']) + +# ── One-time DB migration ───────────────────────────────────────────────────── +local_resource( + 'db-migrate', + cmd='kubectl exec -n finmind deploy/finmind-backend -- alembic upgrade head', + deps=['deploy/k8s/app-stack.yaml'], + labels=['ops'], +) diff --git a/deploy/helm/finmind/Chart.yaml b/deploy/helm/finmind/Chart.yaml new file mode 100644 index 00000000..05ae07cf --- /dev/null +++ b/deploy/helm/finmind/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: finmind +description: FinMind — personal finance app Helm chart +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - finmind + - finance + - fastapi +maintainers: + - name: daatsuka diff --git a/deploy/helm/finmind/templates/_helpers.tpl b/deploy/helm/finmind/templates/_helpers.tpl new file mode 100644 index 00000000..5b29d530 --- /dev/null +++ b/deploy/helm/finmind/templates/_helpers.tpl @@ -0,0 +1,18 @@ +{{/* +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 }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} diff --git a/deploy/helm/finmind/templates/deployment-backend.yaml b/deploy/helm/finmind/templates/deployment-backend.yaml new file mode 100644 index 00000000..fc7d5dd5 --- /dev/null +++ b/deploy/helm/finmind/templates/deployment-backend.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "finmind.fullname" . }}-backend + labels: + app: {{ include "finmind.name" . }}-backend +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ include "finmind.name" . }}-backend + template: + metadata: + labels: + app: {{ include "finmind.name" . }}-backend + spec: + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - containerPort: {{ .Values.backend.port }} + envFrom: + - secretRef: + name: {{ include "finmind.fullname" . }}-secrets + livenessProbe: + httpGet: + path: /health + port: {{ .Values.backend.port }} + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health + port: {{ .Values.backend.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "finmind.fullname" . }}-backend +spec: + selector: + app: {{ include "finmind.name" . }}-backend + ports: + - port: 80 + targetPort: {{ .Values.backend.port }} diff --git a/deploy/helm/finmind/templates/hpa.yaml b/deploy/helm/finmind/templates/hpa.yaml new file mode 100644 index 00000000..e243537d --- /dev/null +++ b/deploy/helm/finmind/templates/hpa.yaml @@ -0,0 +1,20 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "finmind.fullname" . }}-backend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "finmind.fullname" . }}-backend + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/deploy/helm/finmind/templates/ingress.yaml b/deploy/helm/finmind/templates/ingress.yaml new file mode 100644 index 00000000..cfce085f --- /dev/null +++ b/deploy/helm/finmind/templates/ingress.yaml @@ -0,0 +1,29 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "finmind.fullname" . }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "finmind.fullname" $ }}-{{ .service }} + port: + number: 80 + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/finmind/values.yaml b/deploy/helm/finmind/values.yaml new file mode 100644 index 00000000..ffa5d598 --- /dev/null +++ b/deploy/helm/finmind/values.yaml @@ -0,0 +1,70 @@ +replicaCount: 1 + +backend: + image: + repository: finmind-backend + tag: latest + pullPolicy: IfNotPresent + port: 8000 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +frontend: + image: + repository: finmind-frontend + tag: latest + pullPolicy: IfNotPresent + port: 80 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 70 + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: finmind.example.com + paths: + - path: /api + pathType: Prefix + service: backend + - path: / + pathType: Prefix + service: frontend + tls: + - secretName: finmind-tls + hosts: + - finmind.example.com + +postgresql: + enabled: true + auth: + username: finmind + password: "" + database: finmind + primary: + persistence: + size: 5Gi + +redis: + enabled: true + architecture: standalone + auth: + enabled: false diff --git a/deploy/one-click.sh b/deploy/one-click.sh new file mode 100755 index 00000000..052cc8cf --- /dev/null +++ b/deploy/one-click.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# FinMind — Universal One-Click Deployment Script +# Usage: ./deploy/one-click.sh [platform] +# Platforms: docker | k8s | helm | railway | render | fly | heroku | do +set -euo pipefail + +PLATFORM="${1:-docker}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +info() { echo -e "\033[1;34m[FinMind]\033[0m $*"; } +ok() { echo -e "\033[1;32m[OK]\033[0m $*"; } +error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; } + +case "$PLATFORM" in + docker) + info "Starting FinMind with Docker Compose..." + cd "$ROOT" + cp -n .env.example .env 2>/dev/null || true + docker compose up -d --build + ok "FinMind is running at http://localhost:3000 (frontend) and http://localhost:8000 (backend)" + ;; + + k8s) + info "Deploying to Kubernetes..." + command -v kubectl &>/dev/null || error "kubectl not found" + kubectl apply -f "$ROOT/deploy/k8s/namespace.yaml" + kubectl apply -f "$ROOT/deploy/k8s/app-stack.yaml" + kubectl apply -f "$ROOT/deploy/k8s/monitoring-stack.yaml" + kubectl rollout status deployment/finmind-backend -n finmind + ok "Deployed to Kubernetes" + ;; + + helm) + info "Deploying with Helm..." + command -v helm &>/dev/null || error "helm not found" + helm upgrade --install finmind "$ROOT/deploy/helm/finmind" \ + --namespace finmind --create-namespace \ + --wait --timeout 5m + ok "Helm release 'finmind' deployed" + ;; + + tilt) + info "Starting Tilt dev environment..." + command -v tilt &>/dev/null || error "tilt not found (https://tilt.dev)" + cd "$ROOT" && tilt up + ;; + + railway) + info "Deploying to Railway..." + command -v railway &>/dev/null || error "railway CLI not found" + cd "$ROOT" && railway up + ;; + + render) + info "Render deployment — push to git and Render auto-deploys from render.yaml" + ok "See deploy/platforms/render.yaml for config" + ;; + + fly) + info "Deploying to Fly.io..." + command -v fly &>/dev/null || error "fly CLI not found" + cd "$ROOT" && fly deploy --config deploy/platforms/fly.toml + ;; + + heroku) + info "Deploying to Heroku..." + command -v heroku &>/dev/null || error "heroku CLI not found" + heroku container:push web -a "${HEROKU_APP_NAME:-finmind}" + heroku container:release web -a "${HEROKU_APP_NAME:-finmind}" + ok "Deployed to Heroku" + ;; + + do) + info "Deploying to DigitalOcean App Platform..." + command -v doctl &>/dev/null || error "doctl not found" + doctl apps create --spec deploy/platforms/do-app.yaml + ok "DigitalOcean App created" + ;; + + *) + echo "Usage: $0 [docker|k8s|helm|tilt|railway|render|fly|heroku|do]" + exit 1 + ;; +esac diff --git a/deploy/platforms/do-app.yaml b/deploy/platforms/do-app.yaml new file mode 100644 index 00000000..07ca0064 --- /dev/null +++ b/deploy/platforms/do-app.yaml @@ -0,0 +1,40 @@ +name: finmind +region: nyc +services: + - name: backend + dockerfile_path: packages/backend/Dockerfile + source_dir: packages/backend + http_port: 8000 + instance_count: 1 + instance_size_slug: basic-xxs + health_check: + http_path: /health + envs: + - key: DATABASE_URL + scope: RUN_TIME + type: SECRET + - key: REDIS_URL + scope: RUN_TIME + type: SECRET + + - name: frontend + dockerfile_path: app/Dockerfile + source_dir: app + http_port: 80 + instance_count: 1 + instance_size_slug: basic-xxs + envs: + - key: VITE_API_URL + scope: RUN_TIME + value: ${backend.PUBLIC_URL} + +databases: + - name: finmind-db + engine: PG + version: "16" + size: db-s-dev-database + + - name: finmind-redis + engine: REDIS + version: "7" + size: db-s-dev-database diff --git a/deploy/platforms/fly.toml b/deploy/platforms/fly.toml new file mode 100644 index 00000000..9bd23119 --- /dev/null +++ b/deploy/platforms/fly.toml @@ -0,0 +1,35 @@ +app = "finmind-backend" +primary_region = "nrt" + +[build] + dockerfile = "packages/backend/Dockerfile" + +[env] + PORT = "8000" + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + + [http_service.concurrency] + type = "connections" + hard_limit = 25 + soft_limit = 20 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 + +[checks] + [checks.health] + grace_period = "30s" + interval = "15s" + method = "GET" + path = "/health" + port = 8000 + timeout = "10s" + type = "http" diff --git a/deploy/platforms/heroku.yml b/deploy/platforms/heroku.yml new file mode 100644 index 00000000..9c38b1b1 --- /dev/null +++ b/deploy/platforms/heroku.yml @@ -0,0 +1,10 @@ +build: + docker: + web: + dockerfile: packages/backend/Dockerfile + target: production + frontend: + dockerfile: app/Dockerfile +run: + web: uvicorn app.main:app --host 0.0.0.0 --port $PORT + frontend: nginx -g "daemon off;" diff --git a/deploy/platforms/railway.json b/deploy/platforms/railway.json new file mode 100644 index 00000000..fbc92492 --- /dev/null +++ b/deploy/platforms/railway.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "packages/backend/Dockerfile" + }, + "deploy": { + "startCommand": "uvicorn app.main:app --host 0.0.0.0 --port $PORT", + "healthcheckPath": "/health", + "healthcheckTimeout": 30, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + } +} diff --git a/deploy/platforms/render.yaml b/deploy/platforms/render.yaml new file mode 100644 index 00000000..999f01c3 --- /dev/null +++ b/deploy/platforms/render.yaml @@ -0,0 +1,39 @@ +services: + - type: web + name: finmind-backend + runtime: docker + dockerfilePath: ./packages/backend/Dockerfile + dockerContext: ./packages/backend + envVars: + - key: DATABASE_URL + fromDatabase: + name: finmind-db + property: connectionString + - key: REDIS_URL + fromService: + name: finmind-redis + type: redis + property: connectionString + healthCheckPath: /health + autoDeploy: true + + - type: web + name: finmind-frontend + runtime: docker + dockerfilePath: ./app/Dockerfile + dockerContext: ./app + envVars: + - key: VITE_API_URL + fromService: + name: finmind-backend + type: web + property: host + autoDeploy: true + + - type: redis + name: finmind-redis + plan: free + +databases: + - name: finmind-db + plan: free