learning_ai_common_plat/docs/devops/SINGLE_VM_DEPLOYMENT.md
2026-03-23 18:50:38 -07:00

64 KiB
Raw Blame History

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 416 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
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 510× cheaper than Azure/AWS for dev/staging.


3. Architecture: Docker Compose → K3s Migration Path

Phase 1: Docker Compose (after prerequisite work)

⚠️ Prerequisite: ALL product repos must run docker-prep.sh before building Docker images (see §12 Audit Findings). 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.

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 shareddocker 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)

K3s 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:

# 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
# ~/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: '<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
  # All have file: deps → must run docker-prep.sh first.
  # 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

# 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

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)

# 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)

# 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
  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

# 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

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

Exercise 3: ConfigMap / Secret Rotation

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

# 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)

# 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:

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

~/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

# ── 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, docker-prep.sh, and output:'standalone'. 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

# 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

# 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.

# 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.

# 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
cp learning_ai_trails/scripts/docker-prep.sh <target-repo>/scripts/docker-prep.sh
chmod +x <target-repo>/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 Actionscompatible 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/<org>/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)

# 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=<gitea-token>

# For Docker builds, use the compose service name:
@bytelyst:registry=http://gitea:3300/api/packages/bytelyst/npm/

Publish workflow (CI or manual)

# 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='<local-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:

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:

// 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)

# 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:

# 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.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 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 Actionscompatible 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.