Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/k8s-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Validate Kubernetes manifests

on:
push:
paths:
- 'deploy/k8s/**'
- '.github/workflows/k8s-validate.yml'
pull_request:
paths:
- 'deploy/k8s/**'
- '.github/workflows/k8s-validate.yml'

jobs:
kubeconform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install kubeconform
run: |
curl -sSL https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz \
| tar xz
sudo mv kubeconform /usr/local/bin/

- name: Validate manifests
run: |
kubeconform -summary -ignore-missing-schemas deploy/k8s/*.yaml
34 changes: 34 additions & 0 deletions deploy/k8s/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: neurowealth-config
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
data:
NODE_ENV: "production"
PORT: "3001"
LOG_LEVEL: "info"
TRUST_PROXY: "1"
# Stellar network configuration (non-secret)
STELLAR_NETWORK: "mainnet"
STELLAR_RPC_URL: "https://soroban-rpc.mainnet.stellar.org"
# Contract addresses — override per environment
VAULT_CONTRACT_ID: "REPLACE_WITH_VAULT_CONTRACT_ID"
USDC_TOKEN_ADDRESS: "REPLACE_WITH_USDC_TOKEN_ADDRESS"
# CORS — must be explicit in production
CORS_ORIGINS: "https://app.neurowealth.io"
ALLOWED_ORIGINS: "https://app.neurowealth.io"
# Rate limits (tune per environment)
RATE_LIMIT_MAX: "100"
RATE_LIMIT_WINDOW_MS: "900000"
AUTH_RATE_LIMIT_MAX: "20"
ADMIN_RATE_LIMIT_MAX: "10"
# DLQ alerting
DLQ_ALERT_THRESHOLD: "50"
DLQ_ALERT_COOLDOWN_MS: "900000"
# Agent thresholds
REBALANCE_THRESHOLD_PERCENT: "0.5"
MAX_GAS_PERCENT: "0.1"
# Admin dashboard link for alerts
ADMIN_DASHBOARD_URL: "https://admin.neurowealth.io"
92 changes: 92 additions & 0 deletions deploy/k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: neurowealth-backend
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
app.kubernetes.io/component: api
spec:
# IMPORTANT: The monolith starts an in-process Stellar event listener and
# agent cron loop. Multiple replicas will duplicate event processing and
# scheduled jobs. Keep replicas at 1 until worker/API split is implemented.
# See docs/DEPLOYMENT.md for scaling guidance.
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: neurowealth-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app.kubernetes.io/name: neurowealth-backend
app.kubernetes.io/component: api
spec:
serviceAccountName: neurowealth-backend
securityContext:
runAsNonRoot: true
fsGroup: 1000
terminationGracePeriodSeconds: 35
initContainers:
- name: migrate
image: neurowealth-backend:latest
imagePullPolicy: IfNotPresent
command: ["npx", "prisma", "migrate", "deploy"]
envFrom:
- secretRef:
name: neurowealth-secrets
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
containers:
- name: api
image: neurowealth-backend:latest
imagePullPolicy: IfNotPresent
# Do NOT run migrations in the app container — initContainer handles that.
command: ["node", "dist/index.js"]
ports:
- name: http
containerPort: 3001
protocol: TCP
envFrom:
- configMapRef:
name: neurowealth-config
- secretRef:
name: neurowealth-secrets
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
24 changes: 24 additions & 0 deletions deploy/k8s/hpa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Horizontal Pod Autoscaler — disabled by default because the monolith
# requires a single active consumer for the Stellar event listener and agent cron.
# Enable only after splitting API and worker deployments (see docs/DEPLOYMENT.md).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: neurowealth-backend
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: neurowealth-backend
minReplicas: 1
maxReplicas: 1
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
31 changes: 31 additions & 0 deletions deploy/k8s/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: neurowealth-backend
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
annotations:
# TLS — adjust for your ingress controller (nginx, ALB, etc.)
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100k"
# Optional WAF — enable when your provider supports it
# nginx.ingress.kubernetes.io/enable-modsecurity: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.neurowealth.io
secretName: neurowealth-api-tls
rules:
- host: api.neurowealth.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: neurowealth-backend
port:
number: 3001
39 changes: 39 additions & 0 deletions deploy/k8s/migration-job.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Pre-deploy migration job — run before rolling out a new image version.
# Usage:
# kubectl apply -f deploy/k8s/migration-job.yaml
# kubectl wait --for=condition=complete job/neurowealth-migrate -n neurowealth --timeout=300s
apiVersion: batch/v1
kind: Job
metadata:
name: neurowealth-migrate
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
app.kubernetes.io/component: migration
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/name: neurowealth-backend
app.kubernetes.io/component: migration
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
containers:
- name: migrate
image: neurowealth-backend:latest
imagePullPolicy: IfNotPresent
command: ["npx", "prisma", "migrate", "deploy"]
envFrom:
- secretRef:
name: neurowealth-secrets
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
7 changes: 7 additions & 0 deletions deploy/k8s/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: neurowealth
labels:
app.kubernetes.io/name: neurowealth
app.kubernetes.io/part-of: neurowealth
29 changes: 29 additions & 0 deletions deploy/k8s/secret.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Template only — NEVER commit real secret values.
# Create the live Secret with your secrets manager or:
# kubectl create secret generic neurowealth-secrets \
# --namespace=neurowealth \
# --from-literal=DATABASE_URL='postgresql://...' \
# --from-literal=JWT_SEED='...' \
# ...
apiVersion: v1
kind: Secret
metadata:
name: neurowealth-secrets
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
type: Opaque
stringData:
DATABASE_URL: "postgresql://USER:PASSWORD@HOST:5432/neurowealth"
JWT_SEED: "REPLACE_WITH_64_HEX_CHARS"
WALLET_ENCRYPTION_KEY: "REPLACE_WITH_32_BYTE_HEX"
STELLAR_AGENT_SECRET_KEY: "REPLACE_WITH_STELLAR_SECRET_KEY"
ANTHROPIC_API_KEY: "REPLACE_WITH_ANTHROPIC_KEY"
ADMIN_API_TOKEN: "REPLACE_WITH_ADMIN_TOKEN"
TWILIO_AUTH_TOKEN: "REPLACE_WITH_TWILIO_AUTH_TOKEN"
# Optional — include when using WhatsApp integration
TWILIO_ACCOUNT_SID: ""
# Optional alerting / internal service auth
INTERNAL_SERVICE_TOKEN: ""
SLACK_WEBHOOK_URL: ""
PAGERDUTY_ROUTING_KEY: ""
16 changes: 16 additions & 0 deletions deploy/k8s/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: neurowealth-backend
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: neurowealth-backend
ports:
- name: http
port: 3001
targetPort: http
protocol: TCP
7 changes: 7 additions & 0 deletions deploy/k8s/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: neurowealth-backend
namespace: neurowealth
labels:
app.kubernetes.io/name: neurowealth-backend
Loading
Loading