# 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 (current transition plan) - `learning_ai_common_plat` is already the canonical **`pnpm` workspace** monorepo for shared packages, services, and dashboards. - Node/TypeScript product repos are moving toward **`pnpm` as the long-term standard**, but that migration is still **repo-by-repo** and **incremental**. - During the transition, each repo's Docker/build flow must follow the repo's own: - `packageManager` field - lockfile - Dockerfile - `docker-prep.sh` behavior - This plan does **not** merge all repos into one mega-monorepo. Product repos remain independent repositories. - Once a repo migrates to `pnpm`, it must be fully aligned in the same change set: - no `pnpm-lock.yaml` with `npm ci` - no stale `package-lock.json` - no mixed package-manager assumptions in CI, Docker, or docs > **Migration-impact note:** The deployment architecture in this guide stays the same during the `pnpm` migration (Compose, K3s, ingress, namespaces, VM sizing). The main maintenance surface is Docker/build instructions and dependency-prep flow. The biggest operational risk is stale templates or stale docs after an individual repo migrates. --- ## 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 (after prerequisite work) > **⚠️ Prerequisite:** product repos that still rely on `file:`-based `@bytelyst/*` Docker consumption must run `docker-prep.sh` before building images (see §12 Audit Findings). FlowMonk's current backend/web Docker path is the registry-backed exception and uses repo-root build context instead. All Dockerfiles and `output: 'standalone'` configs are now in place (completed 2026-03-22). During the package-manager transition, each repo's Docker build must follow that repo's declared package manager and lockfile semantics rather than assuming `npm` or `pnpm` globally. 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: **⚠️ Critical prerequisite — run BEFORE `docker compose build`:** ```bash # Pack @bytelyst/* file: dependencies into tarballs for each product repo. # Every product repo has file: refs to ../learning_ai_common_plat/packages/* # which don't resolve inside Docker build context. docker-prep.sh packs them. # The prep flow must preserve each repo's package-manager semantics while rewriting # file: refs for Docker contexts. for repo in learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock \ learning_ai_jarvis_jr learning_ai_peakpulse \ learning_ai_fastgap learning_ai_notes learning_ai_trails learning_ai_local_memory_gpt; do (cd $repo && ./scripts/docker-prep.sh) done ``` ```yaml # ~/code/mygh/docker-compose.ecosystem.yml # NOTE: Most product backends/webs still rely on file: deps to @bytelyst/* packages. # Run docker-prep.sh only for the repos that still require tarball prep (see above). 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: '' 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 # Most still have file: deps → run docker-prep.sh first unless the repo is already registry-backed. # 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 Reality Check The ecosystem code and infra design are already **mostly** aligned for a low-effort scaling path. That does **not** mean the whole end-to-end deployment path is proven yet. ### What is already designed for low-change scaling - service identity, ports, and inter-service URLs are already env/config driven in most backends and dashboards - Compose service boundaries already map cleanly to Kubernetes Deployments + Services - Traefik-based routing already maps cleanly to Kubernetes Ingress resources - resource requests/limits and replica counts are already represented in a K8s-friendly way in the documented manifest examples - the namespace split (`infra`, `platform`, `products`, `web`) already gives a usable organizational model for K8s - moving from 1 replica to N replicas for stateless services should mostly be infra config work, not application rewrites - moving from Docker Desktop K8s to K3s or later managed K8s should mostly reuse the same manifest model ### What is not yet proven enough to call low-change - the shared package distribution path for container builds after the Gitea npm registry migration - the pilot Docker build path from the local Gitea registry - the final removal of `docker-prep.sh` / tarball fallback across the wider ecosystem - the complete image build/publish/deploy workflow for all repos under one consistent registry strategy ### Practical interpretation If we solve the package registry + image build path cleanly, then scaling the running system should mostly mean: - increasing replica counts - applying HPAs where justified - adjusting resource requests and limits - moving from local-path storage to stronger persistent storage where needed - adding worker nodes or moving to a managed cluster That is the sense in which the ecosystem is already mostly configurable and scale-friendly. The remaining work is concentrated more in the **build and package-distribution layer** than in the application service code. --- ## 5.2 Remaining Gaps Before Azure VM / K3s Rollout ### Local Mac rehearsal gaps still open - local Homebrew Gitea package metadata and tarball URLs are not yet reliable across both host installs and Docker builds - pilot Docker builds from the local Gitea registry are still blocked - local Gitea Actions has not yet been fully validated for the package publish + consumer install path - the pilot registry migration is only validated for FlowMonk backend/web host-side usage, not end-to-end across Docker and all surfaces ### Azure VM readiness gaps Before calling the Azure VM rollout low-risk, we still need one validated answer for each of these: - where Gitea runs and what `ROOT_URL` / package URL shape it advertises - how Docker image builds authenticate to and consume the package registry - whether package builds happen on the VM directly, inside CI runners, or via a dedicated package-publish pipeline - how image publishing is handled for K3s / later multi-node rollout - which repos are fully registry-backed versus still temporarily using tarball fallback during migration ### K3s / scaling readiness gaps The application architecture is largely ready for scale-by-configuration, but these operational items still need proof or stronger artifacts: - concrete K8s manifests or Helm values for the full ecosystem, not just examples - image registry strategy for K3s and later multi-node or managed Kubernetes rollout - persistent volume strategy for Gitea, Grafana, Loki, Azurite, and any stateful product workloads - autoscaling thresholds and resource defaults validated under representative load - deployment ordering and health-check gating for infra → platform → products → web surfaces ### Recommended order to close these gaps 1. clear the local Gitea package URL and Docker build problem 2. validate one full pilot path: package publish → Docker build → runtime start 3. validate the same path in local Gitea Actions 4. choose the image registry / image distribution pattern for Azure VM and K3s 5. generate and validate concrete K8s/Helm artifacts for the platform + one pilot product first --- ## 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:v2 ./learning_voice_ai_agent/backend kubectl set image deploy/lysnrai-backend lysnrai-backend=bytelyst/lysnrai-backend:v2 -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='' \ --from-literal=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= 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 (all complete) | Repo | Backend Dockerfile | Web Dockerfile | `docker-prep.sh` | `output:'standalone'` | Package manager state | Lockfile state | Docker template type | Status | | ------------------ | ------------------ | ------------------- | ---------------- | --------------------- | ---------------------------------------- | ---------------------------------- | ------------------------------- | ------------------------------------ | | **LysnrAI** | ✅ | ✅ user-dashboard | ✅ | ✅ (conditional) | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **MindLyst** | ✅ | ✅ | ✅ | ✅ (conditional) | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **ChronoMind** | ✅ | ✅ | ✅ | ✅ (conditional) | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **JarvisJr** | ✅ | ✅ | ✅ | ✅ (conditional) | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **PeakPulse** | ✅ | — (no web) | ✅ | — | No Node web surface in this repo | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **FlowMonk** | ✅ | ✅ | ✅ | ✅ (conditional) | **Pilot candidate** for `pnpm` migration | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready | | **NomGap** | ✅ | ✅ | ✅ | ✅ | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Fixed (added `.tarballs/` COPY) | | **NoteLett** | ✅ | ✅ | ✅ | ✅ | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Fixed (explicit COPY, not `.`) | | **ActionTrail** | ✅ | ✅ | ✅ | ✅ | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready (uses `.tarballs/` pattern) | | **LocalMemGPT** | ✅ | ✅ | ✅ | ✅ | Transitioning toward `pnpm` target | Follow repo-local current lockfile | Repo-specific during transition | ✅ Ready (repo-root build context) | | **admin-web** | — | ✅ (in common-plat) | N/A (`pnpm`) | ✅ (conditional) | `pnpm` workspace today | `pnpm-lock.yaml` via common-plat | `pnpm` workspace template | ✅ Ready | | **tracker-web** | — | ✅ (in common-plat) | N/A (`pnpm`) | ✅ (conditional) | `pnpm` workspace today | `pnpm-lock.yaml` via common-plat | `pnpm` workspace template | ✅ Ready | | **user-dashboard** | — | ✅ (in common-plat) | N/A (`pnpm`) | ✅ (conditional) | `pnpm` workspace today | `pnpm-lock.yaml` via common-plat | `pnpm` workspace template | ✅ Ready | **All 10 product repos now have Dockerfiles, and most still retain `docker-prep.sh` plus `output:'standalone'` where applicable.** Created 2026-03-22. > **Note:** The table above tracks Docker readiness, not completed package-manager migration. For product repos, use each repo's actual `packageManager` field and lockfile until that repo is explicitly migrated to `pnpm`. --- ## 11. Dockerfile Template (reference) > **Critical:** Run `docker-prep.sh` first for product repos that use `@bytelyst/*` `file:` dependencies. The prep step packs those dependencies into `.tarballs/` so Docker builds can resolve them inside the repo's own build context. During the migration window, Dockerfiles must match the repo's package manager and lockfile instead of assuming a single global install command. ### Backend / service template — `npm` repo variant ```dockerfile # Pre-requisite: run ./scripts/docker-prep.sh to pack @bytelyst/* tarballs FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ COPY .tarballs/ ./.tarballs/ RUN npm ci --ignore-scripts COPY tsconfig.json ./ COPY src/ ./src/ RUN npx tsc # Production stage FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production COPY package.json package-lock.json ./ COPY .tarballs/ ./.tarballs/ RUN npm ci --omit=dev --ignore-scripts COPY --from=builder /app/dist ./dist # Copy shared/product.json if the backend reads it at runtime COPY shared/ ./shared/ 2>/dev/null || true EXPOSE ${PORT:-4010} CMD ["node", "dist/server.js"] ``` ### Backend / service template — `pnpm` repo variant ```dockerfile # Pre-requisite: run ./scripts/docker-prep.sh if this repo rewrites @bytelyst/* file: deps FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@10 --activate COPY package.json pnpm-lock.yaml ./ COPY .tarballs/ ./.tarballs/ RUN 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 package.json pnpm-lock.yaml ./ COPY .tarballs/ ./.tarballs/ RUN pnpm install --frozen-lockfile --prod --ignore-scripts COPY --from=builder /app/dist ./dist COPY shared/ ./shared/ 2>/dev/null || true EXPOSE ${PORT:-4010} CMD ["node", "dist/server.js"] ``` ### Web (Next.js 16) — `npm` repo variant > **Prerequisite:** `next.config.ts` MUST have `output: 'standalone'` for the standalone Dockerfile pattern to work. Without it, `.next/standalone/` won't be generated and the COPY will fail. ```dockerfile # Pre-requisite: run ./scripts/docker-prep.sh to pack @bytelyst/* tarballs FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ COPY .tarballs/ ./.tarballs/ RUN npm ci COPY . . # Dummy env vars for Next.js build-time static page collection ENV NEXT_PUBLIC_BACKEND_URL=http://localhost:4010 ENV NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003 RUN npm run build 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 2>/dev/null || true EXPOSE 3000 CMD ["node", "server.js"] ``` ### Web (Next.js 16) — `pnpm` repo variant > **Prerequisite:** `next.config.ts` MUST have `output: 'standalone'` for the standalone Dockerfile pattern to work. Keep the repo's `build` script authoritative, including `--webpack` where required. ```dockerfile # Pre-requisite: run ./scripts/docker-prep.sh to pack @bytelyst/* tarballs when applicable FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@10 --activate COPY package.json pnpm-lock.yaml ./ COPY .tarballs/ ./.tarballs/ RUN pnpm install --frozen-lockfile COPY . . # Dummy env vars for Next.js build-time static page collection ENV NEXT_PUBLIC_BACKEND_URL=http://localhost:4010 ENV NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003 RUN pnpm run build 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 2>/dev/null || true EXPOSE 3000 CMD ["node", "server.js"] ``` > **Template selection rule:** > > - Use the `npm` variant only for repos that are still on `npm` with `package-lock.json` and matching Docker/CI scripts. > - Use the `pnpm` variant for repos that have migrated to `pnpm` and carry `pnpm-lock.yaml` plus aligned CI/Docker/docs. > - Do **not** leave a repo in mixed state after migration. ### docker-prep.sh (for repos that don't have one yet) Copy from `learning_ai_trails/scripts/docker-prep.sh` — it handles both `backend/` and `web/` targets, packs all `file:` refs into `.tarballs/`, and rewrites `package.json` to point at them. The important rule is **behavior**, not shell-script ancestry: - `docker-prep.sh` must support both legacy `npm` repos and migrated `pnpm` repos. - It must **not** hardcode `npm` assumptions into tarball rewrite flow. - It must preserve the repo's package-manager semantics after prep: - keep the correct lockfile - keep the correct install command in Docker/CI - keep `.tarballs/` handling compatible with the repo's active package manager ```bash cp learning_ai_trails/scripts/docker-prep.sh /scripts/docker-prep.sh chmod +x /scripts/docker-prep.sh ``` ## 11.1 Long-Term Package-Manager Migration Roadmap ### End-state - `learning_ai_common_plat` remains the canonical **`pnpm` workspace** monorepo. - Node-based product repos migrate to **`pnpm` over time**. - Product repos remain **independent repositories**, not one combined workspace. - Current `.tarballs/` handling for `@bytelyst/*` remains supported unless it is explicitly simplified later. ### Migration principles - No big-bang migration. - One repo at a time. - Fully green before moving to the next repo. - Do not combine package-manager migration with unrelated dependency upgrades. - Migrate CI, Docker, and docs together in the same repo migration. - No mixed lockfile/package-manager state after migration. ### Phase 0 — policy and checklist - Define package-manager policy. - Define migration checklist. - Define validation gates. ### Pilot - `learning_ai_flowmonk` ### Wave 1 - `learning_ai_trails` - `learning_ai_local_memory_gpt` ### Wave 2 - `learning_ai_notes` - `learning_ai_fastgap` - `learning_ai_clock` ### Wave 3 - `learning_ai_jarvis_jr` - `learning_voice_ai_agent` ### Validation gates per migrated repo A repo is only considered migrated when all of the following are aligned and passing: - install - test - typecheck - build - Docker build - local shared package resolution - docs/CI updated --- ## 11.2 Gitea — Self-Hosted Git + CI + Package Registry ### Why Gitea on the VM? 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** — built-in, eliminates the `docker-prep.sh` tarball workflow ### Current pain: `docker-prep.sh` + `.tgz` tarballs Every product repo has `file:../../learning_ai_common_plat/packages/*` dependencies. These don't resolve inside Docker build contexts, so each repo needs a `docker-prep.sh` that: 1. Builds all `@bytelyst/*` packages in common-plat 2. Packs each into a `.tarballs/*.tgz` file 3. Rewrites package.json `file:` refs → `file:.tarballs/bytelyst-*.tgz` 4. Preserves the repo's active package-manager semantics during the rewrite This is fragile, slow, and must be repeated for every image rebuild. ### Solution: Gitea npm Package Registry Gitea has a built-in npm registry at `http://gitea:3300/api/packages//npm/`. Publishing `@bytelyst/*` packages there means **all product repos install from the registry like any normal npm dependency** — no tarballs, no rewriting, no `docker-prep.sh`. > **Current local rehearsal status on this Mac:** host-side package publishing and host-side consumer installs are validated. Full Docker-side validation against the local Homebrew Gitea instance is still blocked by host/Docker registry URL reachability and tarball URL generation behavior. Treat the Docker portion of this section as the target architecture, not yet a fully cleared local result. #### Setup (one-time) ```bash # 1. Create a Gitea API token (Settings → Applications → Generate Token) # Scopes: package:write, package:read # 2. Configure npm/pnpm to use Gitea registry for @bytelyst scope # In ~/.npmrc or project .npmrc: @bytelyst:registry=http://localhost:3300/api/packages/bytelyst/npm/ //localhost:3300/api/packages/bytelyst/npm/:_authToken= # For Docker builds, use the compose service name: @bytelyst:registry=http://gitea:3300/api/packages/bytelyst/npm/ ``` #### Publish workflow (CI or manual) ```bash # In learning_ai_common_plat — publish all packages after build cd /path/to/learning_ai_common_plat # Build all packages pnpm -r --filter './packages/**' build # Publish via the local helper that uses the proven pnpm-pack-based flow GITEA_NPM_TOKEN='' bash ./scripts/publish-local-gitea-packages.sh ``` This helper is important because raw `npm publish` is not sufficient for packages that contain internal `workspace:*` references. The validated local path uses `pnpm pack` first so workspace dependencies are normalized before the tarball is published to Gitea. Or add a CI step in `learning_ai_common_plat/.gitea/workflows/ci.yml`: ```yaml publish-packages: name: Publish @bytelyst/* to Gitea npm registry runs-on: ubuntu-latest needs: [build-test-typecheck] steps: - name: Pull latest working-directory: /path/to/learning_ai_common_plat run: git pull --ff-only origin main || true - name: Build packages working-directory: /path/to/learning_ai_common_plat run: pnpm -r --filter './packages/**' build - name: Publish to Gitea registry working-directory: /path/to/learning_ai_common_plat env: NPM_TOKEN: ${{ secrets.GITEA_NPM_TOKEN }} run: | echo "@bytelyst:registry=http://gitea:3300/api/packages/bytelyst/npm/" > .npmrc echo "//gitea:3300/api/packages/bytelyst/npm/:_authToken=${NPM_TOKEN}" >> .npmrc for pkg in packages/*/; do (cd "$pkg" && npm publish --registry http://gitea:3300/api/packages/bytelyst/npm/ 2>/dev/null || true) done ``` #### Product repo migration For each product repo, replace `file:` refs with versioned (or `latest`) registry refs: ```jsonc // Before (package.json) { "@bytelyst/auth": "file:../../learning_ai_common_plat/packages/auth", "@bytelyst/config": "file:../../learning_ai_common_plat/packages/config", "@bytelyst/cosmos": "file:../../learning_ai_common_plat/packages/cosmos" } // After { "@bytelyst/auth": "^0.1.0", "@bytelyst/config": "^0.1.0", "@bytelyst/cosmos": "^0.1.0" } ``` With `.npmrc` pointing `@bytelyst` scope to Gitea, host-side `pnpm install` / `npm install` resolves from the local registry successfully in the current local rehearsal. For Docker builds, the final single-VM target is still the same, but local validation on this Mac showed that a Homebrew-hosted Gitea instance may require extra reachability and `ROOT_URL` alignment work before BuildKit can consume package metadata and tarballs reliably. #### Dockerfile simplification (after registry migration) ```dockerfile # NO MORE docker-prep.sh or .tarballs/ needed! FROM node:22-alpine AS builder WORKDIR /app # .npmrc baked in (or passed as build arg) COPY .npmrc ./ COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY tsconfig.json ./ COPY src/ ./src/ RUN pnpm run build FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production COPY .npmrc ./ COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile --prod COPY --from=builder /app/dist ./dist EXPOSE ${PORT:-4010} CMD ["node", "dist/server.js"] ``` #### Comparison: before vs after | Aspect | Current (`docker-prep.sh`) | With Gitea npm registry | | ------------------- | ----------------------------------- | ---------------------------------------- | | **Pre-build step** | Run `docker-prep.sh` per repo | None (registry resolves deps) | | **Package format** | `.tgz` tarballs in `.tarballs/` | Standard npm registry install | | **`package.json`** | Rewritten with `file:.tarballs/...` | Normal `^0.1.0` semver refs | | **Docker build** | Must COPY `.tarballs/` into context | Standard `pnpm install` | | **Version pinning** | Whatever was packed at build time | Explicit semver in lockfile | | **Local dev** | `file:` symlinks (fast, direct) | Can keep `file:` for dev OR use registry | | **CI publish** | N/A | Auto-publish on common-plat push | | **Disk overhead** | ~50 MB tarballs per repo | Centralized in Gitea storage | #### Migration path (incremental) 1. **Phase 0:** Gitea running locally with packages enabled 2. **Phase 1:** Publish `@bytelyst/*` packages to local Gitea using the `pnpm pack`-based helper flow 3. **Phase 2:** Migrate one pilot repo on the host: replace tarball/file refs with semver refs and validate install/typecheck/test 4. **Phase 3:** Validate Docker builds against the local registry 5. **Phase 4:** Only after local Docker validation is green, remove `docker-prep.sh` patterns repo-by-repo 6. **Keep `file:` refs available** for local dev via overrides or link-based workflows if needed ### 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. `file:` Dependencies Break Docker Builds (CRITICAL) **Every** product backend and web has `file:../../learning_ai_common_plat/packages/*` dependencies in package.json. These resolve locally via symlinks but **fail inside Docker** because the sibling repo isn't in the build context. **Pattern:** Each repo needs a `docker-prep.sh` that: 1. Runs `pnpm build` in common-plat 2. Packs each `@bytelyst/*` package into a `.tarballs/*.tgz` 3. Rewrites package.json `file:` refs → `file:.tarballs/bytelyst-*.tgz` 4. Preserves the repo's active package-manager semantics during the rewrite **All 10 repos now have `docker-prep.sh`** (created 2026-03-22). Previously only ActionTrail, LocalMemGPT, NoteLett, NomGap had them. > **Long-term note:** As product repos migrate to `pnpm`, this pattern remains valid. What changes is the repo-local install/runtime contract (`pnpm install --frozen-lockfile` instead of `npm ci`), not the deployment architecture or the need to package `@bytelyst/*` dependencies for isolated Docker contexts. ### F3. NomGap Backend Dockerfile Ignores `file:` Deps (BUG) `@/learning_ai_fastgap/backend/Dockerfile` previously did `COPY package.json → npm ci` without copying `.tarballs/`, which broke `file:` dependency resolution inside Docker. **Status:** ✅ Fixed. The backend Dockerfile was updated to include the `.tarballs/` pattern. ### F4. NoteLett Backend Dockerfile Copies Everything (BUG) `@/learning_ai_notes/backend/Dockerfile` previously used `COPY . .` in the build stage, which pulled in broken `node_modules` symlinks from `file:` dependencies. **Status:** ✅ Fixed. The backend Dockerfile now uses explicit `COPY` steps instead of broad context copy. ### 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_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 Required Work Before Compose Works | Priority | Item | Count | Status | | -------- | -------------------------------------------------------- | ------------- | ---------------------------------------------------------- | | **P0** | Create missing `docker-prep.sh` | 6 repos | ✅ Done (3 created, 3 already existed) | | **P0** | Create missing backend Dockerfiles | 6 repos | ✅ Done | | **P0** | Create missing web Dockerfiles | 5 repos | ✅ Done (4 created, PeakPulse has no web) | | **P0** | Add `output: 'standalone'` to next.config.ts | 3 webs | ✅ Done (4 webs: ChronoMind, JarvisJr, FlowMonk, MindLyst) | | **P1** | Fix NomGap backend Dockerfile (add `.tarballs/` COPY) | 1 file | ✅ Done | | **P1** | Fix NoteLett backend Dockerfile (explicit COPY, not `.`) | 1 file | ✅ Done | | **P1** | Create `.env.ecosystem` template | 1 file | ✅ Done | | **P2** | Standardize Node.js version to 22-alpine | 4 Dockerfiles | ✅ Done (all new Dockerfiles use 22-alpine) | | **P2** | Add `extra_hosts` for Linux VM Ollama access | 1 service | ✅ Done (`localmemgpt-backend`) | --- ## 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 ~27 services fit in 32 GB RAM. | | **All Dockerized?** | **Yes.** All 10 product repos now have Dockerfiles + docker-prep.sh. | | **Package-manager direction?** | **`pnpm` is the long-term standard** for Node/TS repos, but migration is phased repo-by-repo, not big-bang. | | **Self-hosted CI?** | **Yes.** Gitea + act_runner (~250 MB combined). GitHub Actions–compatible workflows. | | **Package registry?** | **Gitea npm registry** replaces `docker-prep.sh` + `.tgz` tarballs. 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. | | **Time to production K8s?** | Phase 1 (compose) → Phase 2 (Docker Desktop / K3s) → Phase 3 (multi-node) → Phase 4 (managed). Same manifests. |