# ByteLyst Ecosystem — Single-VM Deployment Guide

> Deploy the **entire** ByteLyst ecosystem on one VM, fully Dockerized, with a local Kubernetes layer (Docker Desktop or K3s) for production-readiness practice.

---

## Package-Manager Strategy

- `learning_ai_common_plat` is the canonical **`pnpm` workspace** monorepo for shared packages, services, and dashboards.
- All 10 Node/TypeScript product repos use **`pnpm`** with `pnpm-lock.yaml`.
- Product repos remain **independent repositories**, not one combined workspace.
- All repos consume `@bytelyst/*` packages from the local **Gitea npm registry** (49 packages published).
- All Dockerfiles use **pnpm + BuildKit secret mount** for registry authentication.

---

## 1. Service Inventory

### Shared Infrastructure (common-plat)

| Service                             | Port       | Image                                                                  | RAM Est. |
| ----------------------------------- | ---------- | ---------------------------------------------------------------------- | -------- |
| **platform-service**                | 4003       | Fastify 5 + TS                                                         | ~200 MB  |
| **extraction-service**              | 4005       | Fastify 5 + Python sidecar                                             | ~350 MB  |
| **mcp-server**                      | 4007       | Fastify 5 + TS                                                         | ~150 MB  |
| **Cosmos DB Emulator**              | 8081, 1234 | `mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview` | ~2 GB    |
| **Azurite** (blob)                  | 10000      | `mcr.microsoft.com/azure-storage/azurite`                              | ~100 MB  |
| **Mailpit** (SMTP)                  | 1025, 8025 | `axllent/mailpit`                                                      | ~50 MB   |
| **Traefik** (gateway)               | 80, 8080   | `traefik:v3.3`                                                         | ~100 MB  |
| **Loki** (logs)                     | 3100       | `grafana/loki`                                                         | ~200 MB  |
| **Grafana** (dashboards)            | 3000       | `grafana/grafana`                                                      | ~200 MB  |
| **Gitea** (git + CI + pkg registry) | 3300, 2222 | `gitea/gitea`                                                          | ~150 MB  |
| **act_runner** (CI runner)          | —          | `gitea/act_runner` (or host-mode binary)                               | ~100 MB  |

### Product Backends (Fastify 5 + TypeScript)

| Product                 | Port | RAM Est. |
| ----------------------- | ---- | -------- |
| **LysnrAI** backend     | 4015 | ~150 MB  |
| **MindLyst** backend    | 4014 | ~150 MB  |
| **ChronoMind** backend  | 4011 | ~150 MB  |
| **JarvisJr** backend    | 4012 | ~150 MB  |
| **NomGap** backend      | 4013 | ~150 MB  |
| **PeakPulse** backend   | 4010 | ~150 MB  |
| **FlowMonk** backend    | 4017 | ~150 MB  |
| **NoteLett** backend    | 4016 | ~150 MB  |
| **ActionTrail** backend | 4018 | ~150 MB  |
| **LocalMemGPT** backend | 4019 | ~150 MB  |

### Web Dashboards (Next.js 16)

| Dashboard              | Default Port | Compose Port | RAM Est. | Notes                                             |
| ---------------------- | ------------ | ------------ | -------- | ------------------------------------------------- |
| **admin-web**          | 3000         | **3001**     | ~250 MB  | No port in package.json; must set `PORT=3001` env |
| **user-dashboard-web** | 3002         | 3002         | ~250 MB  | Port set in package.json                          |
| **tracker-web**        | 3003         | 3003         | ~200 MB  | Port set in package.json                          |
| **NomGap** web         | 3040         | 3040         | ~200 MB  | Port set in Dockerfile                            |
| **ChronoMind** web     | 3000         | **3051**     | ~200 MB  | No port override; must set `PORT` env             |
| **JarvisJr** web       | 3000         | **3052**     | ~200 MB  | No port override; must set `PORT` env             |
| **FlowMonk** web       | 3000         | **3053**     | ~200 MB  | No port override; must set `PORT` env             |
| **NoteLett** web       | 3000         | **3054**     | ~200 MB  | Dockerfile EXPOSE 3000; remap in compose          |
| **ActionTrail** web    | 3000         | **3060**     | ~200 MB  | Dockerfile EXPOSE 3000; remap in compose          |
| **LocalMemGPT** web    | 3070         | 3070         | ~200 MB  | Port set in package.json + Dockerfile             |
| **MindLyst** web       | 3050         | 3050         | ~200 MB  | Port set in package.json (`-p 3050`)              |

> **Port conflict warning:** Grafana uses port 3000. admin-web, ChronoMind, JarvisJr, FlowMonk, NoteLett, and ActionTrail webs all default to 3000. The compose file **must** either set `PORT` env var or remap via `ports:` mapping.

### Optional / AI

| Service          | Port  | RAM Est.                  |
| ---------------- | ----- | ------------------------- |
| **Ollama** (LLM) | 11434 | 4–16 GB (model-dependent) |

---

## 2. VM Sizing

### Minimum (dev/staging, no Ollama)

| Spec      | Value            |
| --------- | ---------------- |
| **vCPUs** | 8                |
| **RAM**   | 32 GB            |
| **Disk**  | 100 GB SSD       |
| **OS**    | Ubuntu 24.04 LTS |

**Breakdown:**

- Cosmos Emulator: ~2 GB
- 10 Fastify backends × 150 MB = ~1.5 GB
- 3 shared services × 250 MB = ~0.75 GB
- 11 Next.js webs × 200 MB = ~2.2 GB
- Infra (Traefik, Loki, Grafana, Azurite, Mailpit) = ~0.65 GB
- Gitea + act_runner = ~0.25 GB
- K3s overhead = ~0.5 GB
- **Subtotal: ~7.65 GB** → headroom for spikes + build cache = **32 GB**

### Recommended (with Ollama, small models)

| Spec      | Value                                         |
| --------- | --------------------------------------------- |
| **vCPUs** | 16                                            |
| **RAM**   | 64 GB                                         |
| **Disk**  | 200 GB NVMe SSD                               |
| **GPU**   | Optional NVIDIA T4/A10 for fast LLM inference |
| **OS**    | Ubuntu 24.04 LTS                              |

### Cloud Equivalents

| Provider    | Instance         | vCPU | RAM    | Price (approx)   |
| ----------- | ---------------- | ---- | ------ | ---------------- |
| **Azure**   | Standard_D8s_v5  | 8    | 32 GB  | ~$280/mo         |
| **Azure**   | Standard_D16s_v5 | 16   | 64 GB  | ~$560/mo         |
| **AWS**     | m6i.2xlarge      | 8    | 32 GB  | ~$280/mo         |
| **AWS**     | m6i.4xlarge      | 16   | 64 GB  | ~$560/mo         |
| **Hetzner** | CPX51            | 16   | 32 GB  | ~$45/mo          |
| **Hetzner** | CCX63            | 48   | 192 GB | ~$230/mo         |
| **Home**    | Mac Mini M4 Pro  | 12   | 48 GB  | One-time ~$1,600 |

> **Cost tip:** Hetzner is 5–10× cheaper than Azure/AWS for dev/staging.

---

## 3. Architecture: Docker Compose → K3s Migration Path

### Phase 1: Docker Compose

> **📋 Enhanced plan:** See [`SINGLE_VM_ENHANCED_PLAN.md`](SINGLE_VM_ENHANCED_PLAN.md) for the deployment plan with Coolify, Valkey, Uptime Kuma, and other open-source tooling.

Create a **unified** `docker-compose.ecosystem.yml` that brings everything up.

### Phase 2: Local Kubernetes (Docker Desktop or K3s)

Two options for single-node K8s — both give you **real** `kubectl`, Helm, Ingress, and CRDs identical to production AKS/EKS/GKE.

#### Option A: Docker Desktop Kubernetes (recommended for Mac/Windows dev)

Docker Desktop includes a built-in **kind** (Kubernetes IN Docker) cluster. Enable it in Docker Desktop → Settings → Kubernetes → Enable Kubernetes.

- **Zero install** — checkbox in Docker Desktop, K8s v1.31+ included
- **Images shared** — `docker build` images are immediately available to K8s (no import step!)
- **GUI dashboard** — Docker Desktop shows Deployments, Pods, Services, Ingresses, ConfigMaps, Secrets
- **kubectl pre-configured** — context `docker-desktop` auto-created
- **Helm works** — install via `brew install helm`
- **Best for:** Mac/Windows local development, quick iteration, visual debugging
- **Limitation:** Single-node only, can't add workers (use K3s for multi-node practice)

#### Option B: K3s (recommended for Linux VMs / multi-node practice)

[K3s](https://k3s.io/) is a lightweight, certified Kubernetes distro.

- Production-grade (CNCF certified, used by Rancher)
- Single binary, ~70 MB, installs in 30 seconds
- Built-in Traefik Ingress (you already use Traefik!)
- Built-in local-path StorageClass
- Runs as systemd service (survives reboot)
- Can scale to multi-node later by just joining worker nodes
- **Best for:** Linux VMs, Hetzner/cloud deployment, multi-node scaling practice

---

## 4. Implementation Plan

### 4.1 Phase 1 — Unified Docker Compose

Create `docker-compose.ecosystem.yml` at workspace root (`~/code/mygh/`) that composes all services:

```yaml
# ~/code/mygh/docker-compose.ecosystem.yml
# All repos consume @bytelyst/* from local Gitea npm registry.

services:
  # ══════════════════════════════════════════════════════
  # INFRASTRUCTURE
  # ══════════════════════════════════════════════════════
  cosmos-emulator:
    image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
    ports: ['8081:8081', '1234:1234']
    environment:
      PROTOCOL: http
      ENABLE_EXPLORER: 'true'
    restart: unless-stopped

  azurite:
    image: mcr.microsoft.com/azure-storage/azurite:3.35.0
    command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
    ports: ['10000:10000']
    volumes: [azurite-data:/data]
    restart: unless-stopped

  mailpit:
    image: axllent/mailpit:v1.27.5
    ports: ['1025:1025', '8025:8025']
    restart: unless-stopped

  traefik:
    image: traefik:v3.3
    command:
      - '--api.insecure=true'
      - '--providers.docker=true'
      - '--providers.docker.exposedbydefault=false'
      - '--entrypoints.web.address=:80'
    ports: ['80:80', '8080:8080']
    volumes: ['/var/run/docker.sock:/var/run/docker.sock:ro']
    restart: unless-stopped

  loki:
    image: grafana/loki:3.3.2
    ports: ['3100:3100']
    volumes: [loki-data:/loki]
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.4.0
    ports: ['3000:3000'] # NOTE: many Next.js webs also default to 3000 — avoid conflicts
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: lysnrai
    volumes: [grafana-data:/var/lib/grafana]
    restart: unless-stopped

  # ══════════════════════════════════════════════════════
  # GIT + CI + PACKAGE REGISTRY
  # ══════════════════════════════════════════════════════
  gitea:
    image: gitea/gitea:1.23
    ports: ['3300:3300', '2222:22']
    environment:
      GITEA__server__HTTP_PORT: 3300
      GITEA__server__ROOT_URL: http://gitea:3300
      GITEA__packages__ENABLED: 'true' # Built-in npm/container registry
      GITEA__service__DISABLE_REGISTRATION: 'true'
    volumes:
      - gitea-data:/data
    restart: unless-stopped

  act-runner:
    image: gitea/act_runner:latest
    environment:
      GITEA_INSTANCE_URL: http://gitea:3300
      GITEA_RUNNER_REGISTRATION_TOKEN: '<generate-from-gitea-admin>'
      GITEA_RUNNER_LABELS: 'ubuntu-latest:host'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on: [gitea]
    restart: unless-stopped

  # ══════════════════════════════════════════════════════
  # SHARED SERVICES (common-plat — no file: deps, pnpm workspace handles it)
  # ══════════════════════════════════════════════════════
  platform-service:
    build:
      context: ./learning_ai_common_plat
      dockerfile: services/platform-service/Dockerfile
    ports: ['4003:4003']
    env_file: [.env.ecosystem]
    environment:
      PORT: 4003
      COSMOS_AUTO_INIT: 'true'
    depends_on: [cosmos-emulator, azurite, mailpit]
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.platform.rule=Host(`platform.local`)'
      - 'traefik.http.services.platform.loadbalancer.server.port=4003'
    restart: unless-stopped

  extraction-service:
    build:
      context: ./learning_ai_common_plat
      dockerfile: services/extraction-service/Dockerfile
    ports: ['4005:4005']
    env_file: [.env.ecosystem]
    environment:
      PORT: 4005
    depends_on: [cosmos-emulator]
    restart: unless-stopped

  mcp-server:
    build:
      context: ./learning_ai_common_plat
      dockerfile: services/mcp-server/Dockerfile
    ports: ['4007:4007']
    env_file: [.env.ecosystem]
    environment:
      PORT: 4007
      PLATFORM_SERVICE_URL: http://platform-service:4003
      EXTRACTION_SERVICE_URL: http://extraction-service:4005
    depends_on: [platform-service, extraction-service]
    restart: unless-stopped

  # ══════════════════════════════════════════════════════
  # PRODUCT BACKENDS
  # ActionTrail + LocalMemGPT Dockerfiles use repo-root context.
  # Others use backend/ subdir context.
  # ══════════════════════════════════════════════════════
  lysnrai-backend:
    build: ./learning_voice_ai_agent/backend
    ports: ['4015:4015']
    env_file: [.env.ecosystem]
    environment: { PORT: '4015', SERVICE_NAME: lysnrai-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  mindlyst-backend:
    build: ./learning_multimodal_memory_agents/backend
    ports: ['4014:4014']
    env_file: [.env.ecosystem]
    environment: { PORT: '4014', SERVICE_NAME: mindlyst-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  chronomind-backend:
    build: ./learning_ai_clock/backend
    ports: ['4011:4011']
    env_file: [.env.ecosystem]
    environment: { PORT: '4011', SERVICE_NAME: chronomind-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  jarvisjr-backend:
    build: ./learning_ai_jarvis_jr/backend
    ports: ['4012:4012']
    env_file: [.env.ecosystem]
    environment: { PORT: '4012', SERVICE_NAME: jarvisjr-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  nomgap-backend:
    build: ./learning_ai_fastgap/backend
    ports: ['4013:4013']
    env_file: [.env.ecosystem]
    environment: { PORT: '4013', SERVICE_NAME: nomgap-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  peakpulse-backend:
    build: ./learning_ai_peakpulse/backend
    ports: ['4010:4010']
    env_file: [.env.ecosystem]
    environment: { PORT: '4010', SERVICE_NAME: peakpulse-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  flowmonk-backend:
    build:
      context: ./learning_ai_flowmonk
      dockerfile: backend/Dockerfile
    ports: ['4017:4017']
    env_file: [.env.ecosystem]
    environment: { PORT: '4017', SERVICE_NAME: flowmonk-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  notelett-backend:
    build: ./learning_ai_notes/backend
    ports: ['4016:4016']
    env_file: [.env.ecosystem]
    environment: { PORT: '4016', SERVICE_NAME: notelett-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  actiontrail-backend:
    build:
      context: ./learning_ai_trails # Dockerfile expects repo-root context
      dockerfile: backend/Dockerfile
    ports: ['4018:4018']
    env_file: [.env.ecosystem]
    environment: { PORT: '4018', SERVICE_NAME: actiontrail-backend }
    depends_on: [platform-service]
    restart: unless-stopped

  localmemgpt-backend:
    build:
      context: ./learning_ai_local_memory_gpt # Dockerfile expects repo-root context
      dockerfile: backend/Dockerfile
    ports: ['4019:4019']
    env_file: [.env.ecosystem]
    environment: { PORT: '4019', OLLAMA_URL: 'http://host.docker.internal:11434' }
    volumes: [localmemgpt-data:/app/db]
    restart: unless-stopped

  # ══════════════════════════════════════════════════════
  # WEB DASHBOARDS
  # IMPORTANT: Most webs default to port 3000 internally.
  # Use PORT env var to override, or remap via host:container ports.
  # ══════════════════════════════════════════════════════
  admin-web:
    build: ./learning_ai_common_plat/dashboards/admin-web
    ports: ['3001:3001']
    env_file: [.env.ecosystem]
    environment:
      PORT: 3001 # admin-web has NO port override — defaults to 3000 without this!
    depends_on: [platform-service]
    restart: unless-stopped

  user-dashboard:
    build: ./learning_voice_ai_agent/user-dashboard-web
    ports: ['3002:3002']
    env_file: [.env.ecosystem]
    depends_on: [lysnrai-backend]
    restart: unless-stopped

  tracker-web:
    build: ./learning_ai_common_plat/dashboards/tracker-web
    ports: ['3003:3003']
    env_file: [.env.ecosystem]
    depends_on: [platform-service]
    restart: unless-stopped

  nomgap-web:
    build: ./learning_ai_fastgap/web
    ports: ['3040:3040']
    environment:
      PORT: 3040
      NEXT_PUBLIC_NOMGAP_API_URL: http://nomgap-backend:4013/api
      NEXT_PUBLIC_PLATFORM_SERVICE_URL: http://platform-service:4003/api
    depends_on: [nomgap-backend]
    restart: unless-stopped

  actiontrail-web:
    build: ./learning_ai_trails/web
    ports: ['3060:3000'] # Internal 3000 → external 3060
    environment:
      NEXT_PUBLIC_API_URL: http://actiontrail-backend:4018
    depends_on: [actiontrail-backend]
    restart: unless-stopped

  localmemgpt-web:
    build:
      context: ./learning_ai_local_memory_gpt # Dockerfile expects repo-root context
      dockerfile: web/Dockerfile
    ports: ['3070:3070']
    environment:
      NEXT_PUBLIC_BACKEND_URL: http://localmemgpt-backend:4019
    depends_on: [localmemgpt-backend]
    restart: unless-stopped

  notelett-web:
    build: ./learning_ai_notes/web
    ports: ['3054:3000'] # Internal 3000 → external 3054
    environment:
      NEXT_PUBLIC_BACKEND_URL: http://notelett-backend:4016
    depends_on: [notelett-backend]
    restart: unless-stopped

  chronomind-web:
    build: ./learning_ai_clock/web
    ports: ['3051:3000'] # Internal 3000 → external 3051
    environment:
      NEXT_PUBLIC_BACKEND_URL: http://chronomind-backend:4011
      NEXT_PUBLIC_PLATFORM_SERVICE_URL: http://platform-service:4003
    depends_on: [chronomind-backend]
    restart: unless-stopped

  jarvisjr-web:
    build: ./learning_ai_jarvis_jr/web
    ports: ['3052:3000'] # Internal 3000 → external 3052
    environment:
      NEXT_PUBLIC_BACKEND_URL: http://jarvisjr-backend:4012
      NEXT_PUBLIC_PLATFORM_SERVICE_URL: http://platform-service:4003
    depends_on: [jarvisjr-backend]
    restart: unless-stopped

  flowmonk-web:
    build:
      context: ./learning_ai_flowmonk
      dockerfile: web/Dockerfile
    ports: ['3053:3000'] # Internal 3000 → external 3053
    environment:
      NEXT_PUBLIC_BACKEND_URL: http://flowmonk-backend:4017
      NEXT_PUBLIC_PLATFORM_SERVICE_URL: http://platform-service:4003
    depends_on: [flowmonk-backend]
    restart: unless-stopped

  mindlyst-web:
    build: ./learning_multimodal_memory_agents/mindlyst-native/web
    ports: ['3050:3050']
    environment:
      PORT: 3050 # package.json sets -p 3050
      NEXT_PUBLIC_BACKEND_URL: http://mindlyst-backend:4014
      NEXT_PUBLIC_PLATFORM_SERVICE_URL: http://platform-service:4003
    depends_on: [mindlyst-backend]
    restart: unless-stopped

volumes:
  azurite-data:
  loki-data:
  grafana-data:
  gitea-data:
  localmemgpt-data:
```

### 4.2 Phase 2 — Local Kubernetes (Docker Desktop or K3s)

#### Install K3s on the VM

```bash
# Install K3s (30 seconds, includes kubectl + containerd)
curl -sfL https://get.k3s.io | sh -

# Verify
sudo kubectl get nodes
# NAME       STATUS   ROLES                  AGE   VERSION
# myvm       Ready    control-plane,master   30s   v1.30.x+k3s1

# Copy kubeconfig for non-root usage
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
```

#### Namespace Layout

```bash
kubectl create namespace bytelyst-infra      # Cosmos, Azurite, Mailpit, Loki, Grafana
kubectl create namespace bytelyst-platform   # platform-service, extraction, mcp
kubectl create namespace bytelyst-products   # 10 product backends
kubectl create namespace bytelyst-web        # All Next.js dashboards
```

#### Example K8s Manifest (one backend)

```yaml
# k8s/products/lysnrai-backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: lysnrai-backend
  namespace: bytelyst-products
  labels:
    app: lysnrai-backend
    product: lysnrai
spec:
  replicas: 1 # Scale to 2+ when ready
  selector:
    matchLabels:
      app: lysnrai-backend
  template:
    metadata:
      labels:
        app: lysnrai-backend
    spec:
      containers:
        - name: lysnrai-backend
          image: bytelyst/lysnrai-backend:latest
          ports:
            - containerPort: 4015
          envFrom:
            - configMapRef:
                name: bytelyst-common-config
            - secretRef:
                name: bytelyst-secrets
          env:
            - name: PORT
              value: '4015'
            - name: SERVICE_NAME
              value: lysnrai-backend
          resources:
            requests:
              memory: '128Mi'
              cpu: '100m'
            limits:
              memory: '256Mi'
              cpu: '500m'
          livenessProbe:
            httpGet:
              path: /health
              port: 4015
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /health
              port: 4015
            initialDelaySeconds: 5
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: lysnrai-backend
  namespace: bytelyst-products
spec:
  selector:
    app: lysnrai-backend
  ports:
    - port: 4015
      targetPort: 4015
```

#### Ingress (Traefik, built into K3s)

```yaml
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: bytelyst-ingress
  namespace: bytelyst-products
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
  rules:
    - host: lysnrai.local
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: lysnrai-backend
                port:
                  number: 4015
    - host: platform.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: platform-service
                port:
                  number: 4003
    # ... repeat per product
```

---

## 5. Docker Compose → K3s Migration Cheat Sheet

| Docker Compose            | K3s Equivalent                             |
| ------------------------- | ------------------------------------------ |
| `services:`               | `Deployment` + `Service`                   |
| `ports:`                  | `Service` (ClusterIP/NodePort)             |
| `env_file:`               | `ConfigMap` + `Secret`                     |
| `depends_on:`             | `initContainers` or readiness probes       |
| `volumes:`                | `PersistentVolumeClaim` (local-path)       |
| `restart: unless-stopped` | Built-in (K8s always restarts pods)        |
| `labels: traefik.*`       | `Ingress` resource                         |
| `docker compose up`       | `kubectl apply -k k8s/`                    |
| `docker compose logs`     | `kubectl logs -f deploy/X` or Loki/Grafana |
| `docker compose ps`       | `kubectl get pods -A`                      |
| Scale: change nothing     | `kubectl scale deploy/X --replicas=3`      |

---

## 5.1 Scaling-Readiness

The ecosystem is designed for low-effort scaling:

- Service identity, ports, and inter-service URLs are env/config driven across all backends and dashboards
- Compose service boundaries map cleanly to Kubernetes Deployments + Services
- Traefik-based routing maps cleanly to Kubernetes Ingress resources
- The namespace split (`infra`, `platform`, `products`, `web`) provides a usable organizational model for K8s
- All repos consume `@bytelyst/*` from Gitea npm registry — Docker builds use standard `pnpm install` with BuildKit secret mount
- Moving from 1 replica to N replicas for stateless services is infra config work, not application rewrites
- Moving from Docker Desktop K8s to K3s or managed K8s reuses the same manifest model

### Scaling the running system means

- Increasing replica counts
- Applying HPAs where justified
- Adjusting resource requests and limits
- Moving from local-path storage to persistent storage where needed
- Adding worker nodes or moving to a managed cluster

---

## 5.2 Remaining Work Before K3s Rollout

### Validated ✅

- Host-side `pnpm install` against local Gitea registry (all 10 repos)
- Docker builds for all 10 repos with BuildKit secret mount
- 49 `@bytelyst/*` packages published to Gitea npm registry
- Local Gitea CI green for 8/8 repos with workflows
- Docker builds verified for MindLyst (backend + web) and LysnrAI (backend + dashboard)
- 1,591 backend tests passing, 9/9 web typechecks clean

### K3s readiness — still needed

- Concrete K8s manifests or Helm values for the full ecosystem (not just examples)
- Image registry strategy for K3s (Gitea container registry or local import)
- Persistent volume strategy for Gitea, Grafana, Loki, Azurite, and stateful workloads
- Autoscaling thresholds and resource defaults validated under representative load
- Deployment ordering and health-check gating for infra → platform → products → web

---

## 6. K3s Practice Exercises (on single VM)

These exercises simulate real production scenarios:

### Exercise 1: Rolling Update

```bash
# Build new image, deploy with zero downtime
docker build -t bytelyst/lysnrai-backend:1.1.0 ./learning_voice_ai_agent/backend
kubectl set image deploy/lysnrai-backend lysnrai-backend=bytelyst/lysnrai-backend:1.1.0 -n bytelyst-products
kubectl rollout status deploy/lysnrai-backend -n bytelyst-products
```

### Exercise 2: Scale Horizontally

```bash
kubectl scale deploy/platform-service --replicas=3 -n bytelyst-platform
# Traefik auto-balances across all 3 pods
```

### Exercise 3: ConfigMap / Secret Rotation

```bash
kubectl create secret generic bytelyst-secrets \
  --from-literal=JWT_SECRET='<replace-with-real-jwt-secret>' \
  --from-literal=COSMOS_KEY='<replace-with-real-cosmos-key>' \
  -n bytelyst-platform --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deploy -n bytelyst-platform
```

### Exercise 4: Resource Limits + HPA

```yaml
# Auto-scale platform-service 1→5 pods based on CPU
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: platform-service-hpa
  namespace: bytelyst-platform
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: platform-service
  minReplicas: 1
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
```

### Exercise 5: Helm Chart (packaged deploy)

```bash
# Create chart scaffold
helm create bytelyst-ecosystem
# Templatize all 25+ services into one chart
# Deploy: helm install bytelyst ./bytelyst-ecosystem -n bytelyst
```

---

## 7. Scaling Path: Single VM → Multi-Node

```
Phase 1: Docker Compose          Phase 2: Local K8s (1 node)
┌─────────────────────┐          ┌──────────────────────────────┐
│  Single VM / Mac     │    →     │  Docker Desktop K8s (kind)   │
│  docker compose up   │          │  or K3s on Linux VM          │
│  ~25 containers      │          │  kubectl apply -k · ~25 pods │
└─────────────────────┘          └──────────────────────────────┘
                                          │
                                          ▼
Phase 3: K3s (3 nodes)           Phase 4: Managed K8s
┌──────────────────────┐         ┌──────────────────────┐
│  1 server + 2 agents │    →    │  AKS / EKS / GKE     │
│  Same manifests!     │         │  Same manifests!      │
│  Real HA             │         │  Auto-scaling nodes   │
└──────────────────────┘         └──────────────────────┘
```

**Docker Desktop K8s → K3s migration:** Same manifests, just change `kubectl` context.

**Adding a worker node to K3s (Phase 3) is one command:**

```bash
# On the worker VM:
curl -sfL https://get.k3s.io | K3S_URL=https://server-ip:6443 K3S_TOKEN=<token> sh -
```

---

## 8. Recommended Directory Structure

```
~/code/mygh/
├── docker-compose.ecosystem.yml     # Phase 1: all-in-one compose
├── .env.ecosystem                   # Shared env vars
├── k8s/                             # Phase 2: K3s manifests
│   ├── kustomization.yaml           # Kustomize root
│   ├── infra/                       # Cosmos emulator, Azurite, Mailpit, Loki, Grafana
│   ├── platform/                    # platform-service, extraction, mcp
│   ├── products/                    # 10 product backends
│   ├── web/                         # 10+ Next.js dashboards
│   ├── config/                      # ConfigMaps
│   └── secrets/                     # Secrets (gitignored)
├── helm/                            # Phase 3: Helm chart
│   └── bytelyst-ecosystem/
│       ├── Chart.yaml
│       ├── values.yaml
│       └── templates/
└── scripts/
    ├── ecosystem-up.sh              # docker compose -f docker-compose.ecosystem.yml up -d
    ├── ecosystem-k3s-deploy.sh      # kubectl apply -k k8s/
    └── ecosystem-build-all.sh       # Build all Docker images
```

---

## 9. Quick Start Commands

```bash
# ── Phase 1: Docker Compose ───────────────────────────
cd ~/code/mygh

# Build all images (first time, ~15-20 min)
docker compose -f docker-compose.ecosystem.yml build

# Start everything
docker compose -f docker-compose.ecosystem.yml up -d

# Check status
docker compose -f docker-compose.ecosystem.yml ps

# View logs
docker compose -f docker-compose.ecosystem.yml logs -f platform-service

# Tear down
docker compose -f docker-compose.ecosystem.yml down

# ── Phase 2a: Docker Desktop Kubernetes (Mac) ────────
# Enable K8s: Docker Desktop → Settings → Kubernetes → Enable
# Verify:
kubectl config use-context docker-desktop
kubectl get nodes    # Should show: docker-desktop   Ready   control-plane

# Build images (Docker Desktop shares images with K8s — no import needed!)
docker build -t bytelyst/platform-service:latest ./learning_ai_common_plat/services/platform-service

# Deploy all
kubectl apply -k k8s/

# Check pods
kubectl get pods -A

# Port-forward for local access
kubectl port-forward svc/platform-service 4003:4003 -n bytelyst-platform

# Or view everything in Docker Desktop GUI → Kubernetes tab

# ── Phase 2b: K3s (Linux VM) ─────────────────────────
# Build + load images into K3s containerd
docker build -t bytelyst/platform-service:latest ./learning_ai_common_plat/services/platform-service
sudo k3s ctr images import <(docker save bytelyst/platform-service:latest)

# Deploy (same manifests as Docker Desktop!)
kubectl apply -k k8s/
kubectl get pods -A
```

---

## 10. Dockerization Status

| Repo               | Backend Dockerfile | Web Dockerfile      | `output:'standalone'` | Build Context  | Status   |
| ------------------ | ------------------ | ------------------- | --------------------- | -------------- | -------- |
| **LysnrAI**        | ✅                 | ✅ user-dashboard   | ✅ (conditional)      | repo root      | ✅ Ready |
| **MindLyst**       | ✅                 | ✅                  | ✅ (conditional)      | repo root      | ✅ Ready |
| **ChronoMind**     | ✅                 | ✅                  | ✅ (conditional)      | backend/       | ✅ Ready |
| **JarvisJr**       | ✅                 | ✅                  | ✅ (conditional)      | backend/       | ✅ Ready |
| **PeakPulse**      | ✅                 | — (no web)          | —                     | backend/       | ✅ Ready |
| **FlowMonk**       | ✅                 | ✅                  | ✅ (conditional)      | repo root      | ✅ Ready |
| **NomGap**         | ✅                 | ✅                  | ✅                    | backend/       | ✅ Ready |
| **NoteLett**       | ✅                 | ✅                  | ✅                    | backend/       | ✅ Ready |
| **ActionTrail**    | ✅                 | ✅                  | ✅                    | repo root      | ✅ Ready |
| **LocalMemGPT**    | ✅                 | ✅                  | ✅                    | repo root      | ✅ Ready |
| **admin-web**      | —                  | ✅ (in common-plat) | ✅ (conditional)      | pnpm workspace | ✅ Ready |
| **tracker-web**    | —                  | ✅ (in common-plat) | ✅ (conditional)      | pnpm workspace | ✅ Ready |
| **user-dashboard** | —                  | ✅ (in common-plat) | ✅ (conditional)      | pnpm workspace | ✅ Ready |

All repos use pnpm with Gitea npm registry. Docker builds use BuildKit secret mount for registry auth.

---

## 11. Dockerfile Templates (reference)

All Dockerfiles use pnpm with BuildKit secret mount for Gitea npm registry authentication. `next.config.ts` MUST have `output: 'standalone'` for web Dockerfiles.

### Backend template

```dockerfile
FROM node:22-alpine AS builder
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@10 --activate

COPY .npmrc.docker ./.npmrc
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=gitea_npm_token \
    export GITEA_NPM_TOKEN="$(cat /run/secrets/gitea_npm_token)" && \
    pnpm install --frozen-lockfile --ignore-scripts

COPY tsconfig.json ./
COPY src/ ./src/
RUN pnpm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production

RUN corepack enable && corepack prepare pnpm@10 --activate

COPY .npmrc.docker ./.npmrc
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=gitea_npm_token \
    export GITEA_NPM_TOKEN="$(cat /run/secrets/gitea_npm_token)" && \
    pnpm install --frozen-lockfile --prod --ignore-scripts

COPY --from=builder /app/dist ./dist
COPY shared/product.json ../shared/product.json

EXPOSE ${PORT:-4010}
CMD ["node", "dist/server.js"]
```

### Web (Next.js 16) template

```dockerfile
FROM node:22-alpine AS builder
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@10 --activate

COPY .npmrc.docker ./.npmrc
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=gitea_npm_token \
    export GITEA_NPM_TOKEN="$(cat /run/secrets/gitea_npm_token)" && \
    pnpm install --frozen-lockfile

COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
RUN npx next build --webpack

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
```

### Docker build command

```bash
export TOKEN="$(cat ~/.gitea-npm-token)"
docker build \
  --add-host localhost:host-gateway \
  --build-arg GITEA_NPM_HOST=host.docker.internal \
  --secret id=gitea_npm_token,env=TOKEN \
  -f backend/Dockerfile \
  -t bytelyst/product-backend:latest \
  .
```

---

## 11.2 Gitea — Self-Hosted Git + CI + Package Registry

Gitea is a lightweight, self-hosted Git forge (~150 MB RAM) that provides three capabilities in one container:

1. **Git hosting** — mirror or primary for all 13+ repos
2. **CI/CD** — GitHub Actions–compatible workflows via `act_runner`
3. **npm package registry** — 49 `@bytelyst/*` packages published, consumed by all 10 product repos

### Registry configuration

```bash
# .npmrc (host-side + Docker-side) — GITEA_NPM_HOST set by switch-network.sh
# NETWORK=corp → localhost, NETWORK=home → Azure VM (from ~/.gitea_vm_host)
@bytelyst:registry=http://${GITEA_NPM_HOST}:3300/api/packages/bytelyst/npm/
//${GITEA_NPM_HOST}:3300/api/packages/bytelyst/npm/:_authToken=${GITEA_NPM_TOKEN}
```

### Publish workflow

```bash
# Build and publish all @bytelyst/* packages to Gitea
cd learning_ai_common_plat
pnpm -r --filter './packages/**' build
GITEA_NPM_TOKEN='<token>' bash ./scripts/gitea/publish-local-packages.sh
```

The publish helper uses `pnpm pack` first so `workspace:*` references are normalized before publishing to Gitea.

For full details, see [`GITEA_NPM_REGISTRY_MIGRATION.md`](GITEA_NPM_REGISTRY_MIGRATION.md).

### Gitea Container Registry (bonus)

Gitea also provides a built-in **Docker container registry** at `http://gitea:3300/v2/`. Instead of building images and importing them into K3s, you can:

```bash
# Tag and push to Gitea container registry
docker tag bytelyst/platform-service:latest gitea:3300/bytelyst/platform-service:latest
docker push gitea:3300/bytelyst/platform-service:latest

# K3s / K8s pulls from Gitea registry
# In deployment YAML: image: gitea:3300/bytelyst/platform-service:latest
```

This gives you a **complete self-hosted DevOps stack**: Git → CI → npm registry → container registry → deploy.

---

## 12. Audit Findings (Review 2026-03-22)

Systematic code review of all claims in this document against the actual codebase.

### F1. Port Conflicts (CRITICAL)

**Grafana** uses port 3000. The following webs also default to 3000:

- admin-web (no port in package.json)
- ChronoMind web (no port override)
- JarvisJr web (no port override)
- FlowMonk web (no port override)
- NoteLett web (Dockerfile EXPOSE 3000)
- ActionTrail web (Dockerfile EXPOSE 3000)

**Fix:** Set `PORT` env var in compose for each, or use host:container port remapping.

### F2. `@bytelyst/*` Package Distribution for Docker Builds

All repos now consume `@bytelyst/*` from the Gitea npm registry (`^0.1.0` semver refs). Docker builds use BuildKit secret mount for registry authentication.

**Status:** ✅ Resolved. See [`GITEA_NPM_REGISTRY_MIGRATION.md`](GITEA_NPM_REGISTRY_MIGRATION.md) for details.

### F3. NomGap Backend Dockerfile

**Status:** ✅ Fixed. Uses Gitea registry pattern.

### F4. NoteLett Backend Dockerfile

**Status:** ✅ Fixed. Uses explicit `COPY` steps with Gitea registry pattern.

### F5. Missing `output: 'standalone'` in next.config.ts (CRITICAL)

The Dockerfile template copies from `.next/standalone/` — this directory only exists when `output: 'standalone'` is set in `next.config.ts`.

| Web            | Has `output: 'standalone'`? | Notes                                                             |
| -------------- | --------------------------- | ----------------------------------------------------------------- |
| NomGap         | ✅                          | Set directly                                                      |
| NoteLett       | ✅                          | Set directly                                                      |
| ActionTrail    | ✅                          | Set directly                                                      |
| LocalMemGPT    | ✅                          | Set directly                                                      |
| admin-web      | ✅                          | Conditional: `process.env.VERCEL ? {} : { output: 'standalone' }` |
| tracker-web    | ✅                          | Conditional (same)                                                |
| user-dashboard | ✅                          | Conditional (same)                                                |
| ChronoMind     | ✅                          | Added 2026-03-22 (conditional)                                    |
| JarvisJr       | ✅                          | Added 2026-03-22 (conditional)                                    |
| FlowMonk       | ✅                          | Added 2026-03-22 (conditional)                                    |
| MindLyst       | ✅                          | Added 2026-03-22 (conditional)                                    |

### F6. Build Context Mismatch for ActionTrail + LocalMemGPT

Their Dockerfiles expect repo-root as build context (they `COPY backend/...` and `COPY shared/...`). The compose `build:` must use `context: ./repo-name` + `dockerfile: backend/Dockerfile`, not `build: ./repo-name/backend`.

**Already correct in the compose above.** Calling it out so future editors don't "simplify" it.

### F7. Node.js Version Inconsistency

Existing Dockerfiles use mixed Node versions:

- NomGap, NoteLett: `node:20-alpine`
- ActionTrail, LocalMemGPT: `node:22-alpine` / `node:22-slim`

**Recommendation:** Standardize on `node:22-alpine` for all new Dockerfiles. Existing ones work but should be updated for consistency.

### F8. Missing `--webpack` Flag for Next.js Builds

Several web apps require `--webpack` flag for builds (Serwist PWA incompatible with Turbopack, or `@bytelyst/*` file: ref transpilation). The Dockerfile template should call the repo's package-manager-appropriate build command (`npm run build` or `pnpm run build`) and that script should map to `next build --webpack` where required.

### F9. Missing `.env.ecosystem` Template

The compose references `.env.ecosystem`. A working template now exists at the workspace root. Current key vars:

```env
# .env.ecosystem — shared env for all services
COSMOS_ENDPOINT=http://cosmos-emulator:8081
COSMOS_KEY=<cosmos-emulator-key-or-local-placeholder>
COSMOS_DATABASE=bytelyst
DB_PROVIDER=cosmos
JWT_SECRET=dev-ecosystem-secret-do-not-use-in-production
AZURE_BLOB_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=...;BlobEndpoint=http://azurite:10000/devstoreaccount1;
PLATFORM_SERVICE_URL=http://platform-service:4003
EXTRACTION_SERVICE_URL=http://extraction-service:4005
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_FROM=noreply@bytelyst.local
FIELD_ENCRYPT_KEY_PROVIDER=env
FIELD_ENCRYPT_KEY=<64-char-hex-dev-key>
FIELD_ENCRYPT_MEK_NAME=dev-ecosystem-mek
NODE_ENV=production
CORS_ORIGIN=*
LOG_LEVEL=info
```

**Status:** ✅ Implemented as `.env.ecosystem` in the workspace root.

### F10. `host.docker.internal` Only Works on Docker Desktop (Mac/Windows)

LocalMemGPT uses `OLLAMA_URL: 'http://host.docker.internal:11434'` — this works on Docker Desktop but **not on Linux VMs** (which is the likely deployment target).

**Status:** ✅ Addressed in `docker-compose.ecosystem.yml` via `extra_hosts: ['host.docker.internal:host-gateway']` on `localmemgpt-backend`.

### Summary of Audit Items

All audit findings have been resolved:

| Priority | Item                                          | Status  |
| -------- | --------------------------------------------- | ------- |
| **P0**   | All backend + web Dockerfiles created         | ✅ Done |
| **P0**   | `output: 'standalone'` in all web configs     | ✅ Done |
| **P0**   | Gitea npm registry for `@bytelyst/*` packages | ✅ Done |
| **P1**   | `.env.ecosystem` template                     | ✅ Done |
| **P2**   | Standardize Node.js version to 22-alpine      | ✅ Done |
| **P2**   | `extra_hosts` for Linux VM Ollama access      | ✅ Done |

---

## 13. Kubernetes Roadmap Reference

Kubernetes planning has been split into a standalone roadmap:

- `docs/devops/KUBERNETES_ROADMAP.md`

Use that document for:

- Docker Compose → Docker Desktop K8s / K3s transition planning
- K8s best practices and production comparison takeaways
- Helm values layering and namespace strategy
- secrets progression
- CI/CD guidance for chart/image promotion
- local K8s deployment workflow shape

`SINGLE_VM_DEPLOYMENT.md` remains the source of truth for:

- single-VM deployment scope
- Docker Compose ecosystem architecture
- Dockerization/package-manager deployment guidance
- current implementation status and audit findings

---

## Summary

| Question                       | Answer                                                                                                         |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| **Can deploy on single VM?**   | **Yes.** All ~35 containers fit in 32 GB RAM.                                                                  |
| **All Dockerized?**            | **Yes.** All 10 product repos + 3 shared services + 3 dashboards have Dockerfiles.                             |
| **Package manager?**           | **pnpm** across all repos. `@bytelyst/*` packages from Gitea npm registry.                                     |
| **Self-hosted CI?**            | **Yes.** Gitea + act_runner (~250 MB combined). GitHub Actions–compatible workflows.                           |
| **Package registry?**          | **Gitea npm registry** — 49 `@bytelyst/*` packages published. Also has Docker container registry.              |
| **K8s practice on single VM?** | **Docker Desktop K8s** (Mac/Windows) or **K3s** (Linux). Same manifests scale to AKS/EKS/GKE.                  |
| **Recommended VM?**            | 8 vCPU / 32 GB (min) or 16 vCPU / 64 GB (with Ollama). Hetzner ~€45/mo for dev.                                |
| **Enhanced tooling?**          | See [`SINGLE_VM_ENHANCED_PLAN.md`](SINGLE_VM_ENHANCED_PLAN.md) — Coolify, Valkey, Uptime Kuma, SOPS.           |
| **Time to production K8s?**    | Phase 1 (compose) → Phase 2 (Docker Desktop / K3s) → Phase 3 (multi-node) → Phase 4 (managed). Same manifests. |
