feat(infra): add production-grade k3s Kubernetes setup for single VM

Complete K8s deployment alternative to Docker Compose, targeting
~50 beta users on a Standard_D8s_v5 Azure VM (8 vCPU, 32 GB RAM).

setup-k8s.sh (6 phases):
  1. Pre-flight: verify docker phases 1-5 ran, disk/RAM checks
  2. Install k3s: Docker runtime, NodePort range 1024-32767
  3. Build images: docker compose build + tag as bytelyst/<svc>
  4. Config: namespaces, ConfigMap (3 copies), Secrets (JWT + blob keys), Ollama
  5. Deploy: infra -> platform -> dashboards -> products (ordered)
  6. Health check: 32 endpoints + kubectl pod status

K8s manifests (18 files):
  - 4 namespaces (infra, platform, dashboards, products)
  - 6 infra (cosmos StatefulSet+PVC, azurite StatefulSet+PVC,
    mailpit, loki StatefulSet+PVC, grafana+PVC, ollama external)
  - 3 platform (Deployment+Service+NodePort each)
  - 2 dashboards (Deployment+Service+NodePort each)
  - 10 backends + 9 webs (all with readiness+liveness probes,
    resource limits, product-specific NEXT_PUBLIC_* env vars)

Design decisions:
  - k3s --docker: reuses existing Docker images, no containerd import
  - Same ports as Docker Compose (NodePort with extended range)
  - ConfigMap replaces .env.ecosystem, copied to 3 app namespaces
  - Blob storage keys injected at deploy time via Secret (not in YAML)
  - Cross-namespace DNS: <svc>.<ns>.svc for service discovery
  - Ollama as Endpoints+Service pointing to host node IP
  - Resource limits: ~19 Gi total, fits in 32 GB with 13 GB headroom
  - Teardown: --teardown flag deletes namespaces, keeps k3s
This commit is contained in:
saravanakumardb1 2026-03-24 14:45:19 -07:00
parent 7d0c469858
commit 8a568932b4
18 changed files with 3175 additions and 222 deletions

View File

@ -1,18 +1,30 @@
# ByteLyst Single-VM Kubernetes Deployment (k3s)
> Deploy the ByteLyst ecosystem on Kubernetes using **k3s** — a lightweight, certified K8s distribution
> that runs on a single VM with ~512 MB overhead.
**Status:** Planning — see design decisions below.
> Deploy the **entire ByteLyst ecosystem** (30 services, 10 products) on Kubernetes
> using **k3s** — a lightweight, CNCF-certified K8s distribution.
> Production-grade for ~50 beta users on a single Azure VM.
---
## Quick Start
```bash
# Step 1: Run Docker setup phases 1-5 (system deps, Gitea, repos, packages)
cd /opt/bytelyst/learning_ai_common_plat/docs/devops/single_azure_vm
sudo ./docker/setup.sh --resume # Runs phases 1-5 (skip 6-8)
# Step 2: Deploy to Kubernetes
sudo ./k8s/setup-k8s.sh # 6 phases: preflight → k3s → images → config → deploy → health
# Step 3: Verify
/opt/bytelyst/check-health-k8s.sh # 32 health checks
kubectl get pods -A # All pods
```
## Prerequisites
Same VM as the Docker Compose approach:
- **Azure VM:** Ubuntu 24.04 LTS, **Standard_D8s_v5** (8 vCPU, 32 GB RAM)
- **Disk:** 128 GB+
- **Docker images:** Built by `docker/setup.sh` phases 1-5 (reused, not rebuilt)
- **Azure VM:** Ubuntu 24.04 LTS, **Standard_D8s_v5** (8 vCPU, 32 GB RAM, 128 GB disk)
- **Docker setup phases 1-5 completed** (system deps, Gitea, repos, packages built + published)
## Why k3s?
@ -59,252 +71,156 @@ Ubuntu 24.04 VM
└── Gitea (Docker container — :3300, used for build-time only)
```
## Manifest Structure (planned)
## File Structure
```
k8s/
├── README.md # This file
├── setup-k8s.sh # Bootstrap script (installs k3s, applies manifests)
├── setup-k8s.sh # Bootstrap script (6 phases)
├── namespaces.yaml # 4 namespaces
├── config/
│ ├── configmap.yaml # Shared env vars (replaces .env.ecosystem)
│ └── secrets.yaml # JWT_SECRET, COSMOS_KEY, etc.
│ └── secrets.yaml # JWT_SECRET template (generated at deploy)
├── infra/
│ ├── cosmos-emulator.yaml # StatefulSet + Service + PVC
│ ├── azurite.yaml # StatefulSet + Service + PVC
│ ├── mailpit.yaml # Deployment + Service
│ ├── loki.yaml # StatefulSet + Service + PVC
│ └── grafana.yaml # Deployment + Service + PVC
│ ├── cosmos-emulator.yaml # StatefulSet + Service + PVC + NodePort
│ ├── azurite.yaml # StatefulSet + Service + PVC + NodePort
│ ├── mailpit.yaml # Deployment + Service + NodePort
│ ├── loki.yaml # StatefulSet + Service + PVC + NodePort
│ ├── grafana.yaml # Deployment + Service + PVC + NodePort
│ └── ollama-external.yaml # Service + Endpoints → host Ollama
├── platform/
│ ├── platform-service.yaml # Deployment + Service
│ ├── extraction-service.yaml # Deployment + Service
│ └── mcp-server.yaml # Deployment + Service
│ ├── platform-service.yaml # Deployment + Service + NodePort (:4003)
│ ├── extraction-service.yaml # Deployment + Service + NodePort (:4005)
│ └── mcp-server.yaml # Deployment + Service + NodePort (:4007)
├── dashboards/
│ ├── admin-web.yaml # Deployment + Service
│ └── tracker-web.yaml # Deployment + Service
├── products/
│ ├── _backend-template.yaml # Helm-like template (for reference)
│ ├── peakpulse-backend.yaml
│ ├── chronomind-backend.yaml
│ ├── ... (8 more backends)
│ ├── lysnrai-dashboard.yaml
│ ├── chronomind-web.yaml
│ └── ... (7 more web apps)
└── ingress/
└── ingress.yaml # Traefik IngressRoute rules
│ ├── admin-web.yaml # Deployment + Service + NodePort (:3001)
│ └── tracker-web.yaml # Deployment + Service + NodePort (:3003)
└── products/
├── backends.yaml # 10 backend Deployments + Services + NodePorts
└── webs.yaml # 9 web Deployments + Services + NodePorts
```
## Setup Phases
| Phase | Duration | What happens |
|-------|----------|--------------|
| 1. Pre-flight | ~10s | Verify Docker phases 1-5 completed, check disk/RAM |
| 2. Install k3s | ~2 min | k3s with Docker runtime, NodePort range 1024-32767 |
| 3. Build images | ~15 min | Docker compose build + tag as `bytelyst/<service>:latest` |
| 4. Generate config | ~30s | Namespaces, ConfigMap (3 copies), Secrets (JWT), Ollama endpoint |
| 5. Deploy | ~5 min | Apply manifests: infra → platform → dashboards → products |
| 6. Health check | ~1 min | 32 endpoint checks + kubectl pod status |
## Key Design Decisions
### 1. Image Source: Import from Docker
### k3s with Docker Runtime
k3s installed with `--docker` flag — reuses existing Docker daemon and images.
No `containerd` import step needed. Same images used by Docker Compose work directly.
k3s uses containerd, not Docker. We import the Docker-built images:
### 4-Namespace Isolation
- **bytelyst-infra** — Cosmos emulator, Azurite, Mailpit, Loki, Grafana
- **bytelyst-platform** — platform-service, extraction-service, mcp-server
- **bytelyst-dashboards** — admin-web, tracker-web
- **bytelyst-products** — 10 backends + 9 web apps
```bash
# Build images with Docker (phases 1-7 from docker/setup.sh)
docker save platform-service:latest | k3s ctr images import -
ConfigMap + Secrets are copied to all 3 app namespaces by the setup script.
# Or build directly with nerdctl (k3s-native)
nerdctl build -t platform-service:latest -f services/platform-service/Dockerfile .
```
### Cross-Namespace DNS
K8s DNS: `<service>.<namespace>.svc.cluster.local`
- Backends reach Cosmos: `cosmos-emulator.bytelyst-infra.svc:8081`
- Webs reach backends: `flowmonk-backend.bytelyst-products.svc:4017`
- Everything reaches platform: `platform-service.bytelyst-platform.svc:4003`
**Decision:** Import from Docker first (simpler), migrate to nerdctl later.
### Ollama as External Service
Ollama stays on the host (systemd). A headless Service + Endpoints in `bytelyst-infra`
points to the node's internal IP. Pods reach it as `ollama.bytelyst-infra.svc:11434`.
Setup script auto-detects the node IP.
### 2. Cosmos Emulator: StatefulSet with PVC
### NodePort for External Access
All services use the **same ports** as Docker Compose (e.g., `:4003`, `:3002`, `:3030`).
k3s is configured with `--kube-apiserver-arg=service-node-port-range=1024-32767`.
The Cosmos emulator needs persistent storage and specific env vars.
Use a `StatefulSet` (not Deployment) for stable network identity:
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cosmos-emulator
namespace: bytelyst-infra
spec:
replicas: 1
serviceName: cosmos-emulator
template:
spec:
containers:
- name: cosmos
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest
ports:
- containerPort: 8081
- containerPort: 1234
env:
- name: AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE
value: "true"
- name: ENABLE_EXPLORER
value: "true"
resources:
limits:
memory: "3Gi"
cpu: "2"
volumeClaimTemplates:
- metadata:
name: cosmos-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
```
### 3. Ollama: Host Network
Ollama stays as a systemd service on the host. Pods reach it via `hostNetwork`
or a manually created Endpoints + Service pointing to the node IP:
```yaml
apiVersion: v1
kind: Service
metadata:
name: ollama
namespace: bytelyst-products
spec:
ports:
- port: 11434
---
apiVersion: v1
kind: Endpoints
metadata:
name: ollama
namespace: bytelyst-products
subsets:
- addresses:
- ip: 172.17.0.1 # Host IP (node's internal IP)
ports:
- port: 11434
```
### 4. ConfigMap replaces .env.ecosystem
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bytelyst-config
namespace: bytelyst-platform
data:
COSMOS_ENDPOINT: "http://cosmos-emulator.bytelyst-infra.svc:8081"
COSMOS_DATABASE: "bytelyst"
DB_PROVIDER: "cosmos"
PLATFORM_SERVICE_URL: "http://platform-service.bytelyst-platform.svc:4003"
EXTRACTION_SERVICE_URL: "http://extraction-service.bytelyst-platform.svc:4005"
```
Note: K8s DNS uses `<service>.<namespace>.svc` format for cross-namespace access.
### 5. Secrets for sensitive values
```yaml
apiVersion: v1
kind: Secret
metadata:
name: bytelyst-secrets
type: Opaque
stringData:
JWT_SECRET: "<generated>"
COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
AZURE_BLOB_ACCOUNT_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
```
### 6. Health Checks → Readiness/Liveness Probes
Every backend gets K8s-native probes:
```yaml
readinessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 30
periodSeconds: 30
```
### 7. Resource Limits
### Resource Limits (tuned for 32 GB VM, 50 beta users)
| Service type | CPU request | CPU limit | Memory request | Memory limit |
|-------------|------------|-----------|---------------|-------------|
| Backend | 100m | 500m | 256Mi | 512Mi |
| Web app | 100m | 500m | 256Mi | 512Mi |
| Platform service | 200m | 1000m | 384Mi | 768Mi |
| Cosmos emulator | 1000m | 2000m | 2Gi | 3Gi |
| Ollama | (host) | (host) | (host) | (host) |
| Backend (×10) | 100m | 500m | 256Mi | 512Mi |
| Web app (×9) | 100m | 500m | 256Mi | 512Mi |
| Platform (×3) | 200m | 1000m | 384Mi | 768Mi |
| Cosmos emulator | 500m | 2000m | 2Gi | 3Gi |
| Grafana | 100m | 500m | 128Mi | 256Mi |
| Mailpit / Loki | 50-100m | 500m | 64-128Mi | 512Mi |
| k3s overhead | — | — | — | ~512Mi |
| Ollama (host) | — | — | — | ~3Gi |
| **Total** | | | **~10 Gi** | **~19 Gi** |
## Implementation Phases
Fits comfortably in 32 GB with ~13 GB headroom.
### Phase A: Foundation (Day 1)
- [ ] Install k3s on VM
- [ ] Create 4 namespaces
- [ ] Deploy ConfigMap + Secrets
- [ ] Deploy cosmos-emulator + azurite (StatefulSets)
- [ ] Verify: `kubectl get pods -A` shows infra running
### Readiness + Liveness Probes
Every service gets both:
- **Readiness:** `GET /health` every 10s (traffic only when ready)
- **Liveness:** `GET /health` every 30s (auto-restart on failure)
- Backends: `initialDelaySeconds: 15`, Web apps: `initialDelaySeconds: 15`
- Cosmos emulator: `initialDelaySeconds: 60` (slow startup)
### Phase B: Platform (Day 1-2)
- [ ] Import platform-service Docker image
- [ ] Deploy platform-service (Deployment + Service)
- [ ] Verify: `kubectl exec` + `curl http://platform-service:4003/health`
- [ ] Deploy extraction-service + mcp-server
- [ ] Deploy admin-web + tracker-web
### Phase C: Products (Day 2-3)
- [ ] Template: create one backend manifest, verify it works
- [ ] Replicate for all 10 backends
- [ ] Create web app manifests (9 services)
- [ ] Verify: all 30 services running
### Phase D: Networking (Day 3)
- [ ] Set up Traefik IngressRoute for external access
- [ ] Configure NodePort services for direct port access
- [ ] Create Ollama external service endpoint
- [ ] Verify: health check script works against K8s services
### Phase E: Operations (Day 4+)
- [ ] `kubectl scale deployment/flowmonk-backend --replicas=2` — test scaling
- [ ] `kubectl rollout restart deployment/platform-service` — test rolling update
- [ ] `kubectl top pods` — resource usage monitoring
- [ ] Set up HorizontalPodAutoscaler for one service
- [ ] Practice: `kubectl logs`, `kubectl exec`, `kubectl describe`
## Useful Commands (cheat sheet)
## Operations Cheat Sheet
```bash
# Cluster status
kubectl get nodes
kubectl get pods -A # All namespaces
kubectl get pods -n bytelyst-products # Product namespace
# ── Cluster status ─────────────────────────────────
kubectl get nodes # Node health
kubectl get pods -A # All pods
kubectl top pods -A # Resource usage (CPU/memory)
# Deploy / update
kubectl apply -f k8s/ # Apply all manifests
kubectl apply -f k8s/products/ # Apply product manifests
kubectl rollout restart deployment/flowmonk-backend -n bytelyst-products
# ── Deploy / update ────────────────────────────────
kubectl apply -f k8s/products/ # Re-apply product manifests
kubectl rollout restart deploy/flowmonk-backend -n bytelyst-products # Rolling restart
# Debugging
kubectl logs deployment/platform-service -n bytelyst-platform -f
kubectl describe pod <pod-name> -n bytelyst-platform
kubectl exec -it deployment/platform-service -n bytelyst-platform -- sh
# ── Scaling (for load testing) ─────────────────────
kubectl scale deploy/platform-service --replicas=2 -n bytelyst-platform
kubectl autoscale deploy/flowmonk-backend --min=1 --max=3 --cpu-percent=70 -n bytelyst-products
# Scaling
kubectl scale deployment/flowmonk-backend --replicas=2 -n bytelyst-products
kubectl autoscale deployment/flowmonk-backend --min=1 --max=3 --cpu-percent=70
# ── Debugging ──────────────────────────────────────
kubectl logs deploy/platform-service -n bytelyst-platform -f # Stream logs
kubectl describe pod <name> -n bytelyst-platform # Pod events
kubectl exec -it deploy/platform-service -n bytelyst-platform -- sh # Shell into pod
# Resource monitoring
kubectl top pods -n bytelyst-products
kubectl top nodes
# ── Teardown ───────────────────────────────────────
sudo ./setup-k8s.sh --teardown # Delete all namespaces (keep k3s)
/usr/local/bin/k3s-uninstall.sh # Uninstall k3s completely
```
## Migration from Docker Compose
## Port Map (same as Docker Compose)
Both approaches can coexist on the same VM:
1. `docker/setup.sh` builds images and publishes packages (phases 1-5)
2. `docker compose down` stops the compose stack
3. `setup-k8s.sh` imports images into k3s and applies manifests
4. Both share the same Gitea registry and Ollama instance
| Service | Port | Health check |
|---------|------|-------------|
| Gitea (npm) | 3300 | `http://localhost:3300/api/v1/version` |
| Ollama (LLM) | 11434 | `http://localhost:11434/api/version` |
| Cosmos Explorer | 1234 | `http://localhost:1234` |
| Azurite (Blob) | 10000 | `http://localhost:10000/devstoreaccount1?comp=list` |
| Mailpit UI | 8025 | `http://localhost:8025` |
| Loki | 3100 | `http://localhost:3100/ready` |
| Grafana | 3000 | `http://localhost:3000/api/health` |
| platform-service | 4003 | `/health` |
| extraction-service | 4005 | `/health` |
| mcp-server | 4007 | `/health` |
| admin-web | 3001 | `/` |
| tracker-web | 3003 | `/` |
| Backends | 4010-4019 | `/health` |
| Web apps | 3002, 3030, 3035, 3040, 3045, 3050, 3055, 3060, 3070 | `/` |
## Switching Between Docker Compose and K8s
Both approaches coexist on the same VM:
```bash
# Docker → K8s
cd /opt/bytelyst/learning_ai_common_plat
docker compose -f docker-compose.ecosystem.yml down # Stop compose stack
sudo ../docs/devops/single_azure_vm/k8s/setup-k8s.sh # Deploy to k3s
# K8s → Docker
sudo ./setup-k8s.sh --teardown # Remove k8s resources
sudo ../docker/setup.sh --phase=7 # Re-deploy via compose
```
Both share: Gitea registry (Docker container), Ollama (systemd), and built Docker images.

View File

@ -0,0 +1,77 @@
# Shared configuration for all ByteLyst services.
# Generated by setup-k8s.sh phase 4 — this is the TEMPLATE.
# The setup script replaces JWT_SECRET with a random value at deploy time.
apiVersion: v1
kind: ConfigMap
metadata:
name: bytelyst-config
namespace: bytelyst-platform
labels:
app.kubernetes.io/part-of: bytelyst
data:
# Cosmos DB Emulator
COSMOS_ENDPOINT: "http://cosmos-emulator.bytelyst-infra.svc:8081"
COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
COSMOS_DATABASE: "bytelyst"
DB_PROVIDER: "cosmos"
# Azure Blob Storage (Azurite) — keys in bytelyst-secrets
STORAGE_PROVIDER: "azure"
AZURE_BLOB_ACCOUNT_NAME: "devstoreaccount1"
AZURE_BLOB_PUBLIC_ENDPOINT: "http://localhost:10000/devstoreaccount1"
# Email (Mailpit)
EMAIL_PROVIDER: "smtp"
EMAIL_FROM_ADDRESS: "noreply@bytelyst.local"
EMAIL_FROM_NAME: "ByteLyst"
SMTP_HOST: "mailpit.bytelyst-infra.svc"
SMTP_PORT: "1025"
SMTP_SECURE: "false"
SMTP_USER: ""
SMTP_PASSWORD: ""
# Stripe (test placeholders)
STRIPE_SECRET_KEY: "sk_test_placeholder"
STRIPE_WEBHOOK_SECRET: "whsec_placeholder"
STRIPE_PRICE_PRO: "price_placeholder"
STRIPE_PRICE_ENTERPRISE: "price_placeholder"
# Extraction Service
PYTHON_SIDECAR_URL: "http://localhost:4006"
DEFAULT_MODEL_ID: "gemini-2.5-flash"
GEMINI_API_KEY: "placeholder"
EXTRACTION_QUEUE_BACKEND: "file"
EXTRACTION_QUEUE_FILE: ".data/extraction-jobs.json"
# Cross-service URLs (K8s DNS: <service>.<namespace>.svc)
PLATFORM_SERVICE_URL: "http://platform-service.bytelyst-platform.svc:4003"
EXTRACTION_SERVICE_URL: "http://extraction-service.bytelyst-platform.svc:4005"
MCP_SERVER_URL: "http://mcp-server.bytelyst-platform.svc:4007"
# Telemetry
TELEMETRY_ENABLED: "true"
RATE_LIMIT_STORE_MODE: "datastore"
# Event Bus
EVENT_BUS_BACKEND: "file"
EVENT_BUS_FILE: ".data/platform-events.json"
# Field Encryption
FIELD_ENCRYPT_KEY_PROVIDER: "memory"
# Product Identity
DEFAULT_PRODUCT_ID: "lysnrai"
# Webhooks (disabled)
WEBHOOK_INVITATION_REDEEMED_URL: ""
WEBHOOK_REFERRAL_STATUS_URL: ""
WEBHOOK_WAITLIST_JOINED_URL: ""
# Notifications (disabled)
TELEGRAM_BOT_TOKEN: ""
TELEGRAM_DEFAULT_CHAT_ID: ""
SLACK_WEBHOOK_URL: ""
SLACK_DEFAULT_CHANNEL: ""
# Ollama (host service via ExternalName)
OLLAMA_URL: "http://ollama.bytelyst-infra.svc:11434"

View File

@ -0,0 +1,14 @@
# Sensitive configuration — JWT_SECRET is replaced by setup-k8s.sh at deploy time.
# Cosmos and Azurite keys are well-known emulator keys (public, safe to commit).
apiVersion: v1
kind: Secret
metadata:
name: bytelyst-secrets
namespace: bytelyst-platform
labels:
app.kubernetes.io/part-of: bytelyst
type: Opaque
stringData:
JWT_SECRET: "REPLACE_ME_AT_DEPLOY_TIME"
AZURE_BLOB_ACCOUNT_KEY: "REPLACE_ME_AT_DEPLOY_TIME"
AZURE_BLOB_CONNECTION_STRING: "REPLACE_ME_AT_DEPLOY_TIME"

View File

@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: admin-web
namespace: bytelyst-dashboards
labels:
app: admin-web
tier: dashboard
spec:
replicas: 1
selector:
matchLabels:
app: admin-web
template:
metadata:
labels:
app: admin-web
tier: dashboard
spec:
containers:
- name: admin-web
image: bytelyst/admin-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3001
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3001"
- name: PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: admin-web
namespace: bytelyst-dashboards
spec:
selector:
app: admin-web
ports:
- name: http
port: 3001
targetPort: 3001
---
apiVersion: v1
kind: Service
metadata:
name: admin-web-external
namespace: bytelyst-dashboards
spec:
type: NodePort
selector:
app: admin-web
ports:
- name: http
port: 3001
targetPort: 3001
nodePort: 3001

View File

@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tracker-web
namespace: bytelyst-dashboards
labels:
app: tracker-web
tier: dashboard
spec:
replicas: 1
selector:
matchLabels:
app: tracker-web
template:
metadata:
labels:
app: tracker-web
tier: dashboard
spec:
containers:
- name: tracker-web
image: bytelyst/tracker-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3003
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3003"
- name: PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /
port: 3003
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3003
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: tracker-web
namespace: bytelyst-dashboards
spec:
selector:
app: tracker-web
ports:
- name: http
port: 3003
targetPort: 3003
---
apiVersion: v1
kind: Service
metadata:
name: tracker-web-external
namespace: bytelyst-dashboards
spec:
type: NodePort
selector:
app: tracker-web
ports:
- name: http
port: 3003
targetPort: 3003
nodePort: 3003

View File

@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: azurite
namespace: bytelyst-infra
labels:
app: azurite
spec:
replicas: 1
serviceName: azurite
selector:
matchLabels:
app: azurite
template:
metadata:
labels:
app: azurite
spec:
containers:
- name: azurite
image: mcr.microsoft.com/azure-storage/azurite:latest
command: ["azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0", "-l", "/data"]
ports:
- containerPort: 10000
name: blob
- containerPort: 10001
name: queue
- containerPort: 10002
name: table
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
tcpSocket:
port: 10000
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: azurite-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: azurite-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: azurite
namespace: bytelyst-infra
spec:
selector:
app: azurite
ports:
- name: blob
port: 10000
targetPort: 10000
---
apiVersion: v1
kind: Service
metadata:
name: azurite-external
namespace: bytelyst-infra
spec:
type: NodePort
selector:
app: azurite
ports:
- name: blob
port: 10000
targetPort: 10000
nodePort: 10000

View File

@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cosmos-emulator
namespace: bytelyst-infra
labels:
app: cosmos-emulator
spec:
replicas: 1
serviceName: cosmos-emulator
selector:
matchLabels:
app: cosmos-emulator
template:
metadata:
labels:
app: cosmos-emulator
spec:
containers:
- name: cosmos
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest
ports:
- containerPort: 8081
name: api
- containerPort: 1234
name: explorer
env:
- name: AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE
value: "true"
- name: ENABLE_EXPLORER
value: "true"
- name: AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE
value: "0.0.0.0"
- name: PROTOCOL
value: "http"
resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "3Gi"
readinessProbe:
httpGet:
path: /_explorer/emulator.pem
port: 8081
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
volumeMounts:
- name: cosmos-data
mountPath: /tmp/cosmos/appdata
volumeClaimTemplates:
- metadata:
name: cosmos-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: cosmos-emulator
namespace: bytelyst-infra
spec:
selector:
app: cosmos-emulator
ports:
- name: api
port: 8081
targetPort: 8081
- name: explorer
port: 1234
targetPort: 1234
---
# NodePort for external access to Cosmos Explorer
apiVersion: v1
kind: Service
metadata:
name: cosmos-emulator-external
namespace: bytelyst-infra
spec:
type: NodePort
selector:
app: cosmos-emulator
ports:
- name: explorer
port: 1234
targetPort: 1234
nodePort: 1234

View File

@ -0,0 +1,89 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: bytelyst-infra
labels:
app: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:10.4.0
ports:
- containerPort: 3000
name: http
env:
- name: GF_SECURITY_ADMIN_USER
value: "admin"
- name: GF_SECURITY_ADMIN_PASSWORD
value: "bytelyst"
- name: GF_USERS_ALLOW_SIGN_UP
value: "false"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: grafana-data
mountPath: /var/lib/grafana
volumes:
- name: grafana-data
persistentVolumeClaim:
claimName: grafana-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-data
namespace: bytelyst-infra
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: bytelyst-infra
spec:
selector:
app: grafana
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: grafana-external
namespace: bytelyst-infra
spec:
type: NodePort
selector:
app: grafana
ports:
- name: http
port: 3000
targetPort: 3000
nodePort: 3000

View File

@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: loki
namespace: bytelyst-infra
labels:
app: loki
spec:
replicas: 1
serviceName: loki
selector:
matchLabels:
app: loki
template:
metadata:
labels:
app: loki
spec:
containers:
- name: loki
image: grafana/loki:2.9.0
args: ["-config.file=/etc/loki/local-config.yaml"]
ports:
- containerPort: 3100
name: http
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /ready
port: 3100
initialDelaySeconds: 15
periodSeconds: 10
volumeMounts:
- name: loki-data
mountPath: /loki
volumeClaimTemplates:
- metadata:
name: loki-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: loki
namespace: bytelyst-infra
spec:
selector:
app: loki
ports:
- name: http
port: 3100
targetPort: 3100
---
apiVersion: v1
kind: Service
metadata:
name: loki-external
namespace: bytelyst-infra
spec:
type: NodePort
selector:
app: loki
ports:
- name: http
port: 3100
targetPort: 3100
nodePort: 3100

View File

@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailpit
namespace: bytelyst-infra
labels:
app: mailpit
spec:
replicas: 1
selector:
matchLabels:
app: mailpit
template:
metadata:
labels:
app: mailpit
spec:
containers:
- name: mailpit
image: axllent/mailpit:latest
ports:
- containerPort: 1025
name: smtp
- containerPort: 8025
name: ui
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
readinessProbe:
httpGet:
path: /
port: 8025
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: mailpit
namespace: bytelyst-infra
spec:
selector:
app: mailpit
ports:
- name: smtp
port: 1025
targetPort: 1025
- name: ui
port: 8025
targetPort: 8025
---
apiVersion: v1
kind: Service
metadata:
name: mailpit-external
namespace: bytelyst-infra
spec:
type: NodePort
selector:
app: mailpit
ports:
- name: ui
port: 8025
targetPort: 8025
nodePort: 8025

View File

@ -0,0 +1,26 @@
# Ollama runs on the host as a systemd service, not in K8s.
# This ExternalName service lets pods reach it via K8s DNS:
# ollama.bytelyst-infra.svc:11434
#
# On the VM, the node's internal IP is used. The setup script
# patches the Endpoints IP at deploy time.
apiVersion: v1
kind: Service
metadata:
name: ollama
namespace: bytelyst-infra
spec:
ports:
- port: 11434
targetPort: 11434
---
apiVersion: v1
kind: Endpoints
metadata:
name: ollama
namespace: bytelyst-infra
subsets:
- addresses:
- ip: NODE_IP_PLACEHOLDER
ports:
- port: 11434

View File

@ -0,0 +1,27 @@
apiVersion: v1
kind: Namespace
metadata:
name: bytelyst-infra
labels:
app.kubernetes.io/part-of: bytelyst
---
apiVersion: v1
kind: Namespace
metadata:
name: bytelyst-platform
labels:
app.kubernetes.io/part-of: bytelyst
---
apiVersion: v1
kind: Namespace
metadata:
name: bytelyst-dashboards
labels:
app.kubernetes.io/part-of: bytelyst
---
apiVersion: v1
kind: Namespace
metadata:
name: bytelyst-products
labels:
app.kubernetes.io/part-of: bytelyst

View File

@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: extraction-service
namespace: bytelyst-platform
labels:
app: extraction-service
tier: platform
spec:
replicas: 1
selector:
matchLabels:
app: extraction-service
template:
metadata:
labels:
app: extraction-service
tier: platform
spec:
containers:
- name: extraction-service
image: bytelyst/extraction-service:latest
imagePullPolicy: Never
ports:
- containerPort: 4005
name: http
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4005"
- name: NODE_ENV
value: "production"
envFrom:
- configMapRef:
name: bytelyst-config
- secretRef:
name: bytelyst-secrets
resources:
requests:
cpu: "200m"
memory: "384Mi"
limits:
cpu: "1000m"
memory: "768Mi"
readinessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: extraction-service
namespace: bytelyst-platform
spec:
selector:
app: extraction-service
ports:
- name: http
port: 4005
targetPort: 4005
---
apiVersion: v1
kind: Service
metadata:
name: extraction-service-external
namespace: bytelyst-platform
spec:
type: NodePort
selector:
app: extraction-service
ports:
- name: http
port: 4005
targetPort: 4005
nodePort: 4005

View File

@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
namespace: bytelyst-platform
labels:
app: mcp-server
tier: platform
spec:
replicas: 1
selector:
matchLabels:
app: mcp-server
template:
metadata:
labels:
app: mcp-server
tier: platform
spec:
containers:
- name: mcp-server
image: bytelyst/mcp-server:latest
imagePullPolicy: Never
ports:
- containerPort: 4007
name: http
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4007"
- name: NODE_ENV
value: "production"
envFrom:
- configMapRef:
name: bytelyst-config
- secretRef:
name: bytelyst-secrets
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health
port: 4007
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4007
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server
namespace: bytelyst-platform
spec:
selector:
app: mcp-server
ports:
- name: http
port: 4007
targetPort: 4007
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server-external
namespace: bytelyst-platform
spec:
type: NodePort
selector:
app: mcp-server
ports:
- name: http
port: 4007
targetPort: 4007
nodePort: 4007

View File

@ -0,0 +1,87 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: platform-service
namespace: bytelyst-platform
labels:
app: platform-service
tier: platform
spec:
replicas: 1
selector:
matchLabels:
app: platform-service
template:
metadata:
labels:
app: platform-service
tier: platform
spec:
containers:
- name: platform-service
image: bytelyst/platform-service:latest
imagePullPolicy: Never
ports:
- containerPort: 4003
name: http
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4003"
- name: NODE_ENV
value: "production"
envFrom:
- configMapRef:
name: bytelyst-config
- secretRef:
name: bytelyst-secrets
resources:
requests:
cpu: "200m"
memory: "384Mi"
limits:
cpu: "1000m"
memory: "768Mi"
readinessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: platform-service
namespace: bytelyst-platform
spec:
selector:
app: platform-service
ports:
- name: http
port: 4003
targetPort: 4003
---
apiVersion: v1
kind: Service
metadata:
name: platform-service-external
namespace: bytelyst-platform
spec:
type: NodePort
selector:
app: platform-service
ports:
- name: http
port: 4003
targetPort: 4003
nodePort: 4003

View File

@ -0,0 +1,778 @@
# All 10 product backends — Deployment + ClusterIP Service + NodePort Service
# Each backend: Fastify 5 + TypeScript, /health endpoint, shared ConfigMap + Secrets
# ═══════════════════════════════════════════════════════════════════════
# ── PeakPulse Backend (:4010) ─────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: peakpulse-backend
namespace: bytelyst-products
labels: &pp-labels
app: peakpulse-backend
tier: backend
product: peakpulse
spec:
replicas: 1
selector:
matchLabels:
app: peakpulse-backend
template:
metadata:
labels: *pp-labels
spec:
containers:
- name: backend
image: bytelyst/peakpulse-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4010
env: &backend-env-4010
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4010"
- name: NODE_ENV
value: "production"
envFrom: &backend-envfrom
- configMapRef:
name: bytelyst-config
- secretRef:
name: bytelyst-secrets
resources: &backend-resources
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe: &probe-4010
httpGet:
path: /health
port: 4010
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4010
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: peakpulse-backend
namespace: bytelyst-products
spec:
selector:
app: peakpulse-backend
ports:
- port: 4010
targetPort: 4010
---
apiVersion: v1
kind: Service
metadata:
name: peakpulse-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: peakpulse-backend
ports:
- port: 4010
targetPort: 4010
nodePort: 4010
---
# ── ChronoMind Backend (:4011) ────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: chronomind-backend
namespace: bytelyst-products
labels: &cm-labels
app: chronomind-backend
tier: backend
product: chronomind
spec:
replicas: 1
selector:
matchLabels:
app: chronomind-backend
template:
metadata:
labels: *cm-labels
spec:
containers:
- name: backend
image: bytelyst/chronomind-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4011
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4011"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4011
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4011
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: chronomind-backend
namespace: bytelyst-products
spec:
selector:
app: chronomind-backend
ports:
- port: 4011
targetPort: 4011
---
apiVersion: v1
kind: Service
metadata:
name: chronomind-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: chronomind-backend
ports:
- port: 4011
targetPort: 4011
nodePort: 4011
---
# ── JarvisJr Backend (:4012) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: jarvisjr-backend
namespace: bytelyst-products
labels:
app: jarvisjr-backend
tier: backend
product: jarvisjr
spec:
replicas: 1
selector:
matchLabels:
app: jarvisjr-backend
template:
metadata:
labels:
app: jarvisjr-backend
tier: backend
product: jarvisjr
spec:
containers:
- name: backend
image: bytelyst/jarvisjr-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4012
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4012"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4012
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4012
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: jarvisjr-backend
namespace: bytelyst-products
spec:
selector:
app: jarvisjr-backend
ports:
- port: 4012
targetPort: 4012
---
apiVersion: v1
kind: Service
metadata:
name: jarvisjr-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: jarvisjr-backend
ports:
- port: 4012
targetPort: 4012
nodePort: 4012
---
# ── NomGap Backend (:4013) ────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: nomgap-backend
namespace: bytelyst-products
labels:
app: nomgap-backend
tier: backend
product: nomgap
spec:
replicas: 1
selector:
matchLabels:
app: nomgap-backend
template:
metadata:
labels:
app: nomgap-backend
tier: backend
product: nomgap
spec:
containers:
- name: backend
image: bytelyst/nomgap-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4013
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4013"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4013
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4013
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: nomgap-backend
namespace: bytelyst-products
spec:
selector:
app: nomgap-backend
ports:
- port: 4013
targetPort: 4013
---
apiVersion: v1
kind: Service
metadata:
name: nomgap-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: nomgap-backend
ports:
- port: 4013
targetPort: 4013
nodePort: 4013
---
# ── MindLyst Backend (:4014) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: mindlyst-backend
namespace: bytelyst-products
labels:
app: mindlyst-backend
tier: backend
product: mindlyst
spec:
replicas: 1
selector:
matchLabels:
app: mindlyst-backend
template:
metadata:
labels:
app: mindlyst-backend
tier: backend
product: mindlyst
spec:
containers:
- name: backend
image: bytelyst/mindlyst-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4014
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4014"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4014
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4014
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: mindlyst-backend
namespace: bytelyst-products
spec:
selector:
app: mindlyst-backend
ports:
- port: 4014
targetPort: 4014
---
apiVersion: v1
kind: Service
metadata:
name: mindlyst-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: mindlyst-backend
ports:
- port: 4014
targetPort: 4014
nodePort: 4014
---
# ── LysnrAI Backend (:4015) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: lysnrai-backend
namespace: bytelyst-products
labels:
app: lysnrai-backend
tier: backend
product: lysnrai
spec:
replicas: 1
selector:
matchLabels:
app: lysnrai-backend
template:
metadata:
labels:
app: lysnrai-backend
tier: backend
product: lysnrai
spec:
containers:
- name: backend
image: bytelyst/lysnrai-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4015
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4015"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4015
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4015
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: lysnrai-backend
namespace: bytelyst-products
spec:
selector:
app: lysnrai-backend
ports:
- port: 4015
targetPort: 4015
---
apiVersion: v1
kind: Service
metadata:
name: lysnrai-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: lysnrai-backend
ports:
- port: 4015
targetPort: 4015
nodePort: 4015
---
# ── NoteLett Backend (:4016) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: notelett-backend
namespace: bytelyst-products
labels:
app: notelett-backend
tier: backend
product: notelett
spec:
replicas: 1
selector:
matchLabels:
app: notelett-backend
template:
metadata:
labels:
app: notelett-backend
tier: backend
product: notelett
spec:
containers:
- name: backend
image: bytelyst/notelett-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4016
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4016"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4016
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4016
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: notelett-backend
namespace: bytelyst-products
spec:
selector:
app: notelett-backend
ports:
- port: 4016
targetPort: 4016
---
apiVersion: v1
kind: Service
metadata:
name: notelett-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: notelett-backend
ports:
- port: 4016
targetPort: 4016
nodePort: 4016
---
# ── FlowMonk Backend (:4017) ─────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: flowmonk-backend
namespace: bytelyst-products
labels:
app: flowmonk-backend
tier: backend
product: flowmonk
spec:
replicas: 1
selector:
matchLabels:
app: flowmonk-backend
template:
metadata:
labels:
app: flowmonk-backend
tier: backend
product: flowmonk
spec:
containers:
- name: backend
image: bytelyst/flowmonk-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4017
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4017"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4017
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4017
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: flowmonk-backend
namespace: bytelyst-products
spec:
selector:
app: flowmonk-backend
ports:
- port: 4017
targetPort: 4017
---
apiVersion: v1
kind: Service
metadata:
name: flowmonk-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: flowmonk-backend
ports:
- port: 4017
targetPort: 4017
nodePort: 4017
---
# ── ActionTrail Backend (:4018) ───────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: actiontrail-backend
namespace: bytelyst-products
labels:
app: actiontrail-backend
tier: backend
product: actiontrail
spec:
replicas: 1
selector:
matchLabels:
app: actiontrail-backend
template:
metadata:
labels:
app: actiontrail-backend
tier: backend
product: actiontrail
spec:
containers:
- name: backend
image: bytelyst/actiontrail-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4018
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4018"
- name: NODE_ENV
value: "production"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4018
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4018
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: actiontrail-backend
namespace: bytelyst-products
spec:
selector:
app: actiontrail-backend
ports:
- port: 4018
targetPort: 4018
---
apiVersion: v1
kind: Service
metadata:
name: actiontrail-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: actiontrail-backend
ports:
- port: 4018
targetPort: 4018
nodePort: 4018
---
# ── LocalMemGPT Backend (:4019) ──────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: localmemgpt-backend
namespace: bytelyst-products
labels:
app: localmemgpt-backend
tier: backend
product: localmemgpt
spec:
replicas: 1
selector:
matchLabels:
app: localmemgpt-backend
template:
metadata:
labels:
app: localmemgpt-backend
tier: backend
product: localmemgpt
spec:
containers:
- name: backend
image: bytelyst/localmemgpt-backend:latest
imagePullPolicy: Never
ports:
- containerPort: 4019
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4019"
- name: NODE_ENV
value: "production"
- name: OLLAMA_URL
value: "http://ollama.bytelyst-infra.svc:11434"
envFrom: *backend-envfrom
resources: *backend-resources
readinessProbe:
httpGet:
path: /health
port: 4019
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4019
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: localmemgpt-backend
namespace: bytelyst-products
spec:
selector:
app: localmemgpt-backend
ports:
- port: 4019
targetPort: 4019
---
apiVersion: v1
kind: Service
metadata:
name: localmemgpt-backend-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: localmemgpt-backend
ports:
- port: 4019
targetPort: 4019
nodePort: 4019

View File

@ -0,0 +1,710 @@
# All 9 product web apps — Deployment + ClusterIP Service + NodePort Service
# Each web: Next.js 16, product-specific NEXT_PUBLIC_* env vars
# ═══════════════════════════════════════════════════════════════════════
# ── LysnrAI Dashboard (:3002) ─────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: lysnrai-dashboard
namespace: bytelyst-products
labels:
app: lysnrai-dashboard
tier: web
product: lysnrai
spec:
replicas: 1
selector:
matchLabels:
app: lysnrai-dashboard
template:
metadata:
labels:
app: lysnrai-dashboard
tier: web
product: lysnrai
spec:
containers:
- name: web
image: bytelyst/lysnrai-dashboard:latest
imagePullPolicy: Never
ports:
- containerPort: 3002
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3002"
- name: PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
- name: ACTIONTRAIL_SERVICE_URL
value: "http://actiontrail-backend.bytelyst-products.svc:4018"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
- name: NEXT_PUBLIC_PRODUCT_ID
value: "lysnrai"
resources: &web-resources
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /
port: 3002
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3002
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: lysnrai-dashboard
namespace: bytelyst-products
spec:
selector:
app: lysnrai-dashboard
ports:
- port: 3002
targetPort: 3002
---
apiVersion: v1
kind: Service
metadata:
name: lysnrai-dashboard-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: lysnrai-dashboard
ports:
- port: 3002
targetPort: 3002
nodePort: 3002
---
# ── ChronoMind Web (:3030) ───────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: chronomind-web
namespace: bytelyst-products
labels:
app: chronomind-web
tier: web
product: chronomind
spec:
replicas: 1
selector:
matchLabels:
app: chronomind-web
template:
metadata:
labels:
app: chronomind-web
tier: web
product: chronomind
spec:
containers:
- name: web
image: bytelyst/chronomind-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3030
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3030"
- name: NEXT_PUBLIC_BACKEND_URL
value: "http://chronomind-backend.bytelyst-products.svc:4011"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3030
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3030
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: chronomind-web
namespace: bytelyst-products
spec:
selector:
app: chronomind-web
ports:
- port: 3030
targetPort: 3030
---
apiVersion: v1
kind: Service
metadata:
name: chronomind-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: chronomind-web
ports:
- port: 3030
targetPort: 3030
nodePort: 3030
---
# ── JarvisJr Web (:3035) ─────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: jarvisjr-web
namespace: bytelyst-products
labels:
app: jarvisjr-web
tier: web
product: jarvisjr
spec:
replicas: 1
selector:
matchLabels:
app: jarvisjr-web
template:
metadata:
labels:
app: jarvisjr-web
tier: web
product: jarvisjr
spec:
containers:
- name: web
image: bytelyst/jarvisjr-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3035
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3035"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3035
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3035
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: jarvisjr-web
namespace: bytelyst-products
spec:
selector:
app: jarvisjr-web
ports:
- port: 3035
targetPort: 3035
---
apiVersion: v1
kind: Service
metadata:
name: jarvisjr-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: jarvisjr-web
ports:
- port: 3035
targetPort: 3035
nodePort: 3035
---
# ── FlowMonk Web (:3040) ─────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: flowmonk-web
namespace: bytelyst-products
labels:
app: flowmonk-web
tier: web
product: flowmonk
spec:
replicas: 1
selector:
matchLabels:
app: flowmonk-web
template:
metadata:
labels:
app: flowmonk-web
tier: web
product: flowmonk
spec:
containers:
- name: web
image: bytelyst/flowmonk-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3040
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3040"
- name: NEXT_PUBLIC_API_URL
value: "http://flowmonk-backend.bytelyst-products.svc:4017"
- name: NEXT_PUBLIC_PLATFORM_URL
value: "http://platform-service.bytelyst-platform.svc:4003/api"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3040
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3040
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: flowmonk-web
namespace: bytelyst-products
spec:
selector:
app: flowmonk-web
ports:
- port: 3040
targetPort: 3040
---
apiVersion: v1
kind: Service
metadata:
name: flowmonk-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: flowmonk-web
ports:
- port: 3040
targetPort: 3040
nodePort: 3040
---
# ── NoteLett Web (:3045) ─────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: notelett-web
namespace: bytelyst-products
labels:
app: notelett-web
tier: web
product: notelett
spec:
replicas: 1
selector:
matchLabels:
app: notelett-web
template:
metadata:
labels:
app: notelett-web
tier: web
product: notelett
spec:
containers:
- name: web
image: bytelyst/notelett-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3045
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3045"
- name: NEXT_PUBLIC_NOTES_API_URL
value: "http://notelett-backend.bytelyst-products.svc:4016/api"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003/api"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3045
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3045
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: notelett-web
namespace: bytelyst-products
spec:
selector:
app: notelett-web
ports:
- port: 3045
targetPort: 3045
---
apiVersion: v1
kind: Service
metadata:
name: notelett-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: notelett-web
ports:
- port: 3045
targetPort: 3045
nodePort: 3045
---
# ── MindLyst Web (:3050) ─────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: mindlyst-web
namespace: bytelyst-products
labels:
app: mindlyst-web
tier: web
product: mindlyst
spec:
replicas: 1
selector:
matchLabels:
app: mindlyst-web
template:
metadata:
labels:
app: mindlyst-web
tier: web
product: mindlyst
spec:
containers:
- name: web
image: bytelyst/mindlyst-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3050
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3050"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3050
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3050
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: mindlyst-web
namespace: bytelyst-products
spec:
selector:
app: mindlyst-web
ports:
- port: 3050
targetPort: 3050
---
apiVersion: v1
kind: Service
metadata:
name: mindlyst-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: mindlyst-web
ports:
- port: 3050
targetPort: 3050
nodePort: 3050
---
# ── NomGap Web (:3055) ───────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: nomgap-web
namespace: bytelyst-products
labels:
app: nomgap-web
tier: web
product: nomgap
spec:
replicas: 1
selector:
matchLabels:
app: nomgap-web
template:
metadata:
labels:
app: nomgap-web
tier: web
product: nomgap
spec:
containers:
- name: web
image: bytelyst/nomgap-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3055
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3055"
- name: NEXT_PUBLIC_NOMGAP_API_URL
value: "http://nomgap-backend.bytelyst-products.svc:4013/api"
- name: NEXT_PUBLIC_PLATFORM_SERVICE_URL
value: "http://platform-service.bytelyst-platform.svc:4003/api"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3055
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3055
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: nomgap-web
namespace: bytelyst-products
spec:
selector:
app: nomgap-web
ports:
- port: 3055
targetPort: 3055
---
apiVersion: v1
kind: Service
metadata:
name: nomgap-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: nomgap-web
ports:
- port: 3055
targetPort: 3055
nodePort: 3055
---
# ── ActionTrail Web (:3060) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: actiontrail-web
namespace: bytelyst-products
labels:
app: actiontrail-web
tier: web
product: actiontrail
spec:
replicas: 1
selector:
matchLabels:
app: actiontrail-web
template:
metadata:
labels:
app: actiontrail-web
tier: web
product: actiontrail
spec:
containers:
- name: web
image: bytelyst/actiontrail-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3060
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3060"
- name: NEXT_PUBLIC_API_URL
value: "http://actiontrail-backend.bytelyst-products.svc:4018"
- name: NEXT_PUBLIC_PLATFORM_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3060
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3060
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: actiontrail-web
namespace: bytelyst-products
spec:
selector:
app: actiontrail-web
ports:
- port: 3060
targetPort: 3060
---
apiVersion: v1
kind: Service
metadata:
name: actiontrail-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: actiontrail-web
ports:
- port: 3060
targetPort: 3060
nodePort: 3060
---
# ── LocalMemGPT Web (:3070) ──────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: localmemgpt-web
namespace: bytelyst-products
labels:
app: localmemgpt-web
tier: web
product: localmemgpt
spec:
replicas: 1
selector:
matchLabels:
app: localmemgpt-web
template:
metadata:
labels:
app: localmemgpt-web
tier: web
product: localmemgpt
spec:
containers:
- name: web
image: bytelyst/localmemgpt-web:latest
imagePullPolicy: Never
ports:
- containerPort: 3070
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3070"
- name: NEXT_PUBLIC_BACKEND_URL
value: "http://localmemgpt-backend.bytelyst-products.svc:4019"
- name: NEXT_PUBLIC_PLATFORM_URL
value: "http://platform-service.bytelyst-platform.svc:4003"
resources: *web-resources
readinessProbe:
httpGet:
path: /
port: 3070
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3070
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: localmemgpt-web
namespace: bytelyst-products
spec:
selector:
app: localmemgpt-web
ports:
- port: 3070
targetPort: 3070
---
apiVersion: v1
kind: Service
metadata:
name: localmemgpt-web-ext
namespace: bytelyst-products
spec:
type: NodePort
selector:
app: localmemgpt-web
ports:
- port: 3070
targetPort: 3070
nodePort: 3070

View File

@ -0,0 +1,580 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════
# ByteLyst Single-VM Kubernetes Setup (k3s)
# ═══════════════════════════════════════════════════════════════════════
# Deploys the ByteLyst ecosystem on Kubernetes using k3s.
#
# PREREQUISITE: Run ../docker/setup.sh phases 1-5 first to install
# system deps, Gitea, clone repos, build + publish @bytelyst/* packages.
# This script handles k8s-specific deployment only.
#
# What this script does:
# Phase 1: Pre-flight checks (verify docker phases ran)
# Phase 2: Install k3s (lightweight K8s)
# Phase 3: Build Docker images + import into k3s containerd
# Phase 4: Generate K8s secrets (JWT, etc.)
# Phase 5: Apply K8s manifests (namespaces → config → infra → platform → products)
# Phase 6: Health check
#
# Usage:
# sudo ./setup-k8s.sh # Full install
# sudo ./setup-k8s.sh --phase=N # Run single phase (1-6)
# sudo ./setup-k8s.sh --status # Show phase status
# sudo ./setup-k8s.sh --reset # Clear markers, start fresh
# sudo ./setup-k8s.sh --teardown # Remove all K8s resources
# ═══════════════════════════════════════════════════════════════════════
set -euo pipefail
# ── Configuration ────────────────────────────────────────────────────
INSTALL_DIR="/opt/bytelyst"
K8S_DIR="$(cd "$(dirname "$0")" && pwd)"
STATE_DIR="${INSTALL_DIR}/.setup-state-k8s"
COMPOSE_FILE="docker-compose.ecosystem.yml"
# Well-known emulator keys (public, safe to embed)
COSMOS_EMULATOR_KEY="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
# Image prefix for k3s imports
IMAGE_PREFIX="bytelyst"
# Services that need Docker image builds (not pre-built infra)
BUILD_SERVICES=(
platform-service extraction-service mcp-server
admin-web tracker-web
peakpulse-backend chronomind-backend jarvisjr-backend nomgap-backend
mindlyst-backend lysnrai-backend notelett-backend flowmonk-backend
actiontrail-backend localmemgpt-backend
lysnrai-dashboard chronomind-web jarvisjr-web flowmonk-web notelett-web
mindlyst-web nomgap-web actiontrail-web localmemgpt-web
)
# Namespaces that need the shared ConfigMap + Secrets
CONFIG_NAMESPACES=(bytelyst-platform bytelyst-dashboards bytelyst-products)
# ── Helpers ──────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[k8s]${NC} $*"; }
ok() { echo -e "${GREEN}$*${NC}"; }
warn() { echo -e "${YELLOW}$*${NC}"; }
fail() { echo -e "${RED}$*${NC}" >&2; exit 1; }
mkdir -p "$STATE_DIR"
mark_phase_done() { date -Iseconds > "${STATE_DIR}/phase${1}.done"; }
is_phase_done() { [ -f "${STATE_DIR}/phase${1}.done" ]; }
reset_markers() { rm -f "${STATE_DIR}"/phase*.done; log "Phase markers cleared."; }
last_completed_phase() {
local last=0
for i in 1 2 3 4 5 6; do
if is_phase_done "$i"; then last=$i; else break; fi
done
echo "$last"
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 1: Pre-flight Checks
# ═══════════════════════════════════════════════════════════════════════
phase1_preflight() {
log "Phase 1: Pre-flight checks..."
# Verify docker setup phases 1-5 ran
local docker_state="${INSTALL_DIR}/.setup-state"
for p in 1 2 3 4 5; do
if [ ! -f "${docker_state}/phase${p}.done" ]; then
fail "Docker phase ${p} not completed. Run first: sudo ../docker/setup.sh --resume"
fi
done
ok "Docker phases 1-5 completed"
# Verify repos exist
local plat_dir="${INSTALL_DIR}/learning_ai_common_plat"
[ -d "$plat_dir" ] || fail "Missing ${plat_dir}. Run docker/setup.sh phases 1-5 first."
ok "Repos cloned"
# Verify Docker is available (needed for image builds)
command -v docker &>/dev/null || fail "Docker not found. Run docker/setup.sh phase 1."
ok "Docker available"
# Pre-flight: disk + memory
local disk_gb mem_gb
disk_gb=$(df -BG / | awk 'NR==2 {gsub(/G/,"",$4); print $4}')
mem_gb=$(free -g | awk '/^Mem:/ {print $2}')
log " Disk: ${disk_gb} GB free, RAM: ${mem_gb} GB total"
[ "${disk_gb:-0}" -ge 20 ] || warn "Low disk (${disk_gb} GB). Recommend 40+ GB free."
[ "${mem_gb:-0}" -ge 16 ] || warn "Low RAM (${mem_gb} GB). Recommend 32 GB."
ok "Phase 1 complete. Pre-flight passed."
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 2: Install k3s
# ═══════════════════════════════════════════════════════════════════════
phase2_k3s() {
log "Phase 2: Installing k3s..."
if command -v kubectl &>/dev/null && kubectl cluster-info &>/dev/null 2>&1; then
ok "k3s already installed and running"
else
# Install k3s with:
# --docker: use Docker as container runtime (reuse existing images)
# --disable=traefik: we manage our own ingress
# --write-kubeconfig-mode=644: allow non-root kubectl
# --kube-apiserver-arg: extend NodePort range to use our service ports
log " Installing k3s (Docker runtime, extended NodePort range)..."
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
--docker \
--disable=traefik \
--write-kubeconfig-mode=644 \
--kube-apiserver-arg=service-node-port-range=1024-32767" \
sh -
# Wait for k3s to be ready
log " Waiting for k3s node to be Ready..."
local retries=30
while [ $retries -gt 0 ]; do
if kubectl get nodes 2>/dev/null | grep -q " Ready"; then
break
fi
sleep 5
retries=$((retries - 1))
done
if [ $retries -eq 0 ]; then
fail "k3s node did not become Ready within 150 seconds."
fi
fi
# Verify
kubectl get nodes
ok "Phase 2 complete. k3s is running."
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 3: Build Docker Images + Tag for k3s
# ═══════════════════════════════════════════════════════════════════════
phase3_images() {
log "Phase 3: Building Docker images for k3s..."
local plat_dir="${INSTALL_DIR}/learning_ai_common_plat"
# Restore Gitea token for Docker builds
if [ -z "${GITEA_NPM_TOKEN:-}" ] && [ -f "${INSTALL_DIR}/.gitea_token" ]; then
GITEA_NPM_TOKEN=$(cat "${INSTALL_DIR}/.gitea_token")
export GITEA_NPM_TOKEN
fi
[ -n "${GITEA_NPM_TOKEN:-}" ] || fail "GITEA_NPM_TOKEN not set. Run docker/setup.sh phase 2."
# Detect Docker host IP for Gitea access during builds
local docker_host_ip
docker_host_ip=$(ip -4 addr show docker0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "172.17.0.1")
export GITEA_NPM_HOST="${docker_host_ip}"
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Stop Ollama to free RAM during builds
if systemctl is-active --quiet ollama 2>/dev/null; then
log " Stopping Ollama to free RAM for builds..."
systemctl stop ollama 2>/dev/null || true
fi
# Build images using docker compose (same Dockerfiles as docker approach)
local env_file="${plat_dir}/.env.ecosystem"
if [ ! -f "$env_file" ]; then
# Generate minimal env for compose config parsing
log " Generating temporary .env.ecosystem for compose builds..."
cat > "$env_file" <<-ENV
COSMOS_KEY=${COSMOS_EMULATOR_KEY}
JWT_SECRET=build-time-placeholder
COSMOS_ENDPOINT=http://localhost:8081
COSMOS_DATABASE=bytelyst
DB_PROVIDER=cosmos
ENV
fi
local build_ok=0 build_fail=0
local total=${#BUILD_SERVICES[@]}
local idx=0
mkdir -p "${STATE_DIR}/builds"
for svc in "${BUILD_SERVICES[@]}"; do
idx=$((idx + 1))
log " [${idx}/${total}] Building ${svc}..."
local log_file="${STATE_DIR}/builds/${svc}.log"
if docker compose -f "${plat_dir}/${COMPOSE_FILE}" --env-file "$env_file" \
build "$svc" > "$log_file" 2>&1; then
# Tag for k3s: compose builds as <project>-<svc>, we tag as bytelyst/<svc>
local compose_image
compose_image=$(docker compose -f "${plat_dir}/${COMPOSE_FILE}" --env-file "$env_file" \
images --format json 2>/dev/null | jq -r ".[] | select(.Service==\"${svc}\") | .Repository + \":\" + .Tag" 2>/dev/null || true)
if [ -n "$compose_image" ] && [ "$compose_image" != ":" ]; then
docker tag "$compose_image" "${IMAGE_PREFIX}/${svc}:latest" 2>/dev/null || true
fi
build_ok=$((build_ok + 1))
ok " [${idx}/${total}] ${svc} — built"
else
build_fail=$((build_fail + 1))
warn " [${idx}/${total}] ${svc} — FAILED (see ${log_file})"
fi
done
# Restart Ollama
if command -v ollama &>/dev/null; then
log " Restarting Ollama..."
systemctl start ollama 2>/dev/null || nohup ollama serve > /var/log/ollama.log 2>&1 &
fi
# Prune build cache
docker builder prune -f --filter "until=1h" > /dev/null 2>&1 || true
log " Built: ${build_ok}, Failed: ${build_fail}"
[ "$build_fail" -eq 0 ] || warn " ${build_fail} images failed to build. Fix and re-run: sudo ./setup-k8s.sh --phase=3"
ok "Phase 3 complete. ${build_ok}/${total} images ready."
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 4: Generate K8s Secrets + Patch ConfigMap
# ═══════════════════════════════════════════════════════════════════════
phase4_config() {
log "Phase 4: Generating K8s configuration..."
# Apply namespaces first
log " Creating namespaces..."
kubectl apply -f "${K8S_DIR}/namespaces.yaml"
# Generate random JWT secret
local jwt_secret
jwt_secret=$(openssl rand -base64 32)
# Patch the secrets template with real JWT secret
local secrets_file="${K8S_DIR}/config/secrets.yaml"
log " Generating secrets (JWT_SECRET)..."
# Well-known Azurite emulator key (public, safe)
local azurite_key="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
local azurite_conn="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=${azurite_key};BlobEndpoint=http://azurite.bytelyst-infra.svc:10000/devstoreaccount1;"
# Apply secrets to all namespaces that need them
for ns in "${CONFIG_NAMESPACES[@]}"; do
kubectl create secret generic bytelyst-secrets \
--namespace="$ns" \
--from-literal=JWT_SECRET="$jwt_secret" \
--from-literal=AZURE_BLOB_ACCOUNT_KEY="$azurite_key" \
--from-literal=AZURE_BLOB_CONNECTION_STRING="$azurite_conn" \
--dry-run=client -o yaml | kubectl apply -f -
done
ok "Secrets applied to ${#CONFIG_NAMESPACES[@]} namespaces"
# Detect node IP for Ollama external service
local node_ip
node_ip=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "172.17.0.1")
log " Node IP for Ollama: ${node_ip}"
# Patch Ollama endpoint with actual node IP
sed "s/NODE_IP_PLACEHOLDER/${node_ip}/" "${K8S_DIR}/infra/ollama-external.yaml" \
| kubectl apply -f -
ok "Ollama external service configured (${node_ip}:11434)"
# Apply ConfigMap to all namespaces that need it
for ns in "${CONFIG_NAMESPACES[@]}"; do
sed "s/namespace: bytelyst-platform/namespace: ${ns}/" "${K8S_DIR}/config/configmap.yaml" \
| kubectl apply -f -
done
ok "ConfigMap applied to ${#CONFIG_NAMESPACES[@]} namespaces"
ok "Phase 4 complete."
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 5: Apply K8s Manifests (ordered deployment)
# ═══════════════════════════════════════════════════════════════════════
phase5_deploy() {
log "Phase 5: Deploying services to k3s..."
# ── 5a: Infrastructure ──────────────────────────────────────────────
log " Deploying infrastructure..."
for f in "${K8S_DIR}"/infra/*.yaml; do
local name
name=$(basename "$f" .yaml)
[ "$name" = "ollama-external" ] && continue # Already applied in phase 4
log " Applying ${name}..."
kubectl apply -f "$f"
done
ok "Infrastructure deployed"
# Wait for Cosmos emulator (everything depends on it)
log " Waiting for Cosmos emulator to be Ready (this can take 2-3 minutes)..."
kubectl wait --for=condition=Ready pod -l app=cosmos-emulator \
-n bytelyst-infra --timeout=300s 2>/dev/null || warn "Cosmos emulator not ready yet"
# ── 5b: Platform services ───────────────────────────────────────────
log " Deploying platform services..."
kubectl apply -f "${K8S_DIR}/platform/"
ok "Platform services deployed"
# Wait for platform-service
log " Waiting for platform-service..."
kubectl wait --for=condition=Ready pod -l app=platform-service \
-n bytelyst-platform --timeout=120s 2>/dev/null || warn "platform-service not ready yet"
# ── 5c: Dashboards ─────────────────────────────────────────────────
log " Deploying dashboards..."
kubectl apply -f "${K8S_DIR}/dashboards/"
ok "Dashboards deployed"
# ── 5d: Product services ────────────────────────────────────────────
log " Deploying product backends + web apps..."
kubectl apply -f "${K8S_DIR}/products/"
ok "Product services deployed"
# ── Summary ─────────────────────────────────────────────────────────
log " Waiting 30s for services to stabilize..."
sleep 30
echo ""
log " Pod status across all namespaces:"
kubectl get pods -A -l app.kubernetes.io/part-of=bytelyst 2>/dev/null || \
kubectl get pods -A 2>/dev/null | grep -E "bytelyst-"
echo ""
ok "Phase 5 complete. All manifests applied."
}
# ═══════════════════════════════════════════════════════════════════════
# PHASE 6: Health Check
# ═══════════════════════════════════════════════════════════════════════
phase6_verify() {
log "Phase 6: Verifying service health..."
# Create reusable health check script
cat > "${INSTALL_DIR}/check-health-k8s.sh" <<'HEALTH'
#!/usr/bin/env bash
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
check() {
local name="$1" url="$2"
if curl -sf --connect-timeout 3 "$url" > /dev/null 2>&1; then
echo -e "${GREEN}${name}${NC} ${url}"
else
echo -e "${RED}${name}${NC} ${url}"
fi
}
echo ""
echo "═══ K8s Cluster ═══"
echo -n " Nodes: "; kubectl get nodes --no-headers 2>/dev/null | wc -l | tr -d ' '
echo -n " Pods: "; kubectl get pods -A --no-headers 2>/dev/null | wc -l | tr -d ' '
echo -n " Ready: "; kubectl get pods -A --no-headers 2>/dev/null | grep -c "Running" || echo 0
echo ""
echo "═══ Infrastructure ═══"
check "Gitea (npm)" "http://localhost:3300/api/v1/version"
check "Ollama (LLM)" "http://localhost:11434/api/version"
check "Cosmos Explorer" "http://localhost:1234"
check "Azurite (Blob)" "http://localhost:10000/devstoreaccount1?comp=list"
check "Mailpit" "http://localhost:8025"
check "Loki" "http://localhost:3100/ready"
check "Grafana" "http://localhost:3000/api/health"
echo ""
echo "═══ Platform Services ═══"
check "platform-service" "http://localhost:4003/health"
check "extraction-service" "http://localhost:4005/health"
check "mcp-server" "http://localhost:4007/health"
echo ""
echo "═══ Dashboards ═══"
check "admin-web" "http://localhost:3001"
check "tracker-web" "http://localhost:3003"
echo ""
echo "═══ Product Backends ═══"
check "peakpulse" "http://localhost:4010/health"
check "chronomind" "http://localhost:4011/health"
check "jarvisjr" "http://localhost:4012/health"
check "nomgap" "http://localhost:4013/health"
check "mindlyst" "http://localhost:4014/health"
check "lysnrai" "http://localhost:4015/health"
check "notelett" "http://localhost:4016/health"
check "flowmonk" "http://localhost:4017/health"
check "actiontrail" "http://localhost:4018/health"
check "localmemgpt" "http://localhost:4019/health"
echo ""
echo "═══ Product Web Apps ═══"
check "lysnrai-dashboard" "http://localhost:3002"
check "chronomind-web" "http://localhost:3030"
check "jarvisjr-web" "http://localhost:3035"
check "flowmonk-web" "http://localhost:3040"
check "notelett-web" "http://localhost:3045"
check "mindlyst-web" "http://localhost:3050"
check "nomgap-web" "http://localhost:3055"
check "actiontrail-web" "http://localhost:3060"
check "localmemgpt-web" "http://localhost:3070"
echo ""
echo "═══ K8s Quick Commands ═══"
echo " kubectl get pods -A # All pods"
echo " kubectl top pods -A # Resource usage"
echo " kubectl logs deploy/<name> -n <ns> -f # Stream logs"
echo " kubectl rollout restart deploy/<name> # Rolling restart"
echo ""
HEALTH
chmod +x "${INSTALL_DIR}/check-health-k8s.sh"
# Run it
bash "${INSTALL_DIR}/check-health-k8s.sh"
ok "Phase 6 complete."
}
# ═══════════════════════════════════════════════════════════════════════
# TEARDOWN: Remove all K8s resources
# ═══════════════════════════════════════════════════════════════════════
teardown() {
log "Tearing down all ByteLyst K8s resources..."
for ns in bytelyst-products bytelyst-dashboards bytelyst-platform bytelyst-infra; do
log " Deleting namespace: ${ns}"
kubectl delete namespace "$ns" --ignore-not-found=true 2>/dev/null || true
done
reset_markers
ok "Teardown complete. k3s itself is still running (uninstall with: /usr/local/bin/k3s-uninstall.sh)"
}
# ═══════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════
run_phase() {
local n="$1"
case "$n" in
1) phase1_preflight ;;
2) phase2_k3s ;;
3) phase3_images ;;
4) phase4_config ;;
5) phase5_deploy ;;
6) phase6_verify ;;
*) fail "Unknown phase: $n" ;;
esac
mark_phase_done "$n"
}
usage() {
echo "Usage: sudo ./setup-k8s.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --resume Auto-resume from last completed phase"
echo " --phase=N Run ONLY phase N (1-6)"
echo " --reset Clear phase markers"
echo " --status Show completed phases"
echo " --teardown Remove all K8s resources"
echo " -h, --help Show this help"
echo ""
echo "Phases:"
echo " 1 Pre-flight checks (verify docker phases ran)"
echo " 2 Install k3s"
echo " 3 Build Docker images + tag for k3s"
echo " 4 Generate K8s config (Secrets + ConfigMap)"
echo " 5 Apply manifests (infra → platform → dashboards → products)"
echo " 6 Health check"
echo ""
echo "PREREQUISITE: Run ../docker/setup.sh (phases 1-5) first."
}
main() {
local mode="full" start_phase=1 only_phase=0
for arg in "$@"; do
case "$arg" in
--resume)
mode="resume" ;;
--phase=*)
mode="single"
only_phase="${arg#*=}" ;;
--reset)
reset_markers; exit 0 ;;
--status)
echo "K8s phase completion:"
for i in 1 2 3 4 5 6; do
if is_phase_done "$i"; then
echo " Phase $i: DONE ($(cat "${STATE_DIR}/phase${i}.done"))"
else
echo " Phase $i: pending"
fi
done
exit 0 ;;
--teardown)
teardown; exit 0 ;;
-h|--help)
usage; exit 0 ;;
*)
warn "Unknown option: $arg"; usage; exit 1 ;;
esac
done
mkdir -p "$INSTALL_DIR"
exec > >(tee -a "${INSTALL_DIR}/setup-k8s.log") 2>&1
echo ""
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ByteLyst K8s Deployment (k3s on single VM) ║"
echo "║ 30 services · 4 namespaces · production-grade ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo ""
log "Log file: ${INSTALL_DIR}/setup-k8s.log"
[ "$(id -u)" -eq 0 ] || fail "This script must be run as root (sudo)."
local start_time
start_time=$(date +%s)
if [ "$mode" = "single" ]; then
log "Running ONLY phase ${only_phase}..."
run_phase "$only_phase"
ok "Phase ${only_phase} complete."
exit 0
fi
if [ "$mode" = "resume" ]; then
local last
last=$(last_completed_phase)
if [ "$last" -ge 6 ]; then
ok "All phases completed. Use --reset to start over."
exit 0
fi
start_phase=$((last + 1))
log "Resuming from phase ${start_phase}."
fi
for n in 1 2 3 4 5 6; do
[ "$n" -ge "$start_phase" ] || continue
run_phase "$n"
done
local elapsed=$(( $(date +%s) - start_time ))
local minutes=$(( elapsed / 60 ))
local seconds=$(( elapsed % 60 ))
echo ""
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ K8s deployment complete in ${minutes}m ${seconds}s ║"
echo "║ ║"
echo "║ Health check: /opt/bytelyst/check-health-k8s.sh ║"
echo "║ All pods: kubectl get pods -A ║"
echo "║ Pod resources: kubectl top pods -A ║"
echo "║ Stream logs: kubectl logs deploy/<name> -n <ns> -f ║"
echo "║ Scale up: kubectl scale deploy/<name> --replicas=2 ║"
echo "║ Teardown: sudo ./setup-k8s.sh --teardown ║"
echo "║ ║"
echo "║ Grafana: http://localhost:3000 (admin / bytelyst) ║"
echo "║ Mailpit: http://localhost:8025 ║"
echo "║ Gitea: http://localhost:3300 ║"
echo "║ Ollama: http://localhost:11434 ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo ""
}
main "$@"