learning_ai_common_plat/docs/devops/SINGLE_VM_DEPLOYMENT.md
saravanakumardb1 54a06e227a refactor(scripts): move 5 Gitea scripts into scripts/gitea/ subdirectory
Moved:
  publish-local-gitea-packages.sh  → gitea/publish-local-packages.sh
  publish-outdated-gitea-packages.sh → gitea/publish-outdated-packages.sh
  release-gitea-packages.sh        → gitea/release-packages.sh
  run-registry-tests.sh            → gitea/run-registry-tests.sh
  harden-publish-config.sh         → gitea/harden-publish-config.sh

Dropped -gitea- infix (redundant with folder name).

Fixed in every moved script:
- REPO_ROOT: ../ → ../../ (one level deeper)
- Internal cross-reference comments

Updated all 10 referencing files:
- package.json (release script path)
- .gitea/workflows/ci.yml (publish step)
- 3 workflow .md files (publish-outdated usage)
- 3 devops docs (publish-local + registry-tests refs)
- 2 internal comment cross-references
2026-04-13 00:02:55 -07:00

46 KiB
Raw Permalink 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

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

1. Service Inventory

Shared Infrastructure (common-plat)

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

Product Backends (Fastify 5 + TypeScript)

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

Web Dashboards (Next.js 16)

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

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

Optional / AI

Service Port RAM Est.
Ollama (LLM) 11434 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

📋 Enhanced plan: See SINGLE_VM_ENHANCED_PLAN.md for the deployment plan with Coolify, Valkey, Uptime Kuma, and other open-source tooling.

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

Phase 2: Local Kubernetes (Docker Desktop or K3s)

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Install K3s on the VM

# 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

The ecosystem is designed for low-effort scaling:

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

Scaling the running system means

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

5.2 Remaining Work Before K3s Rollout

Validated

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

K3s readiness — still needed

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

6. K3s Practice Exercises (on single VM)

These exercises simulate real production scenarios:

Exercise 1: Rolling Update

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

Exercise 2: Scale Horizontally

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

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

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


11. Dockerfile Templates (reference)

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

Backend template

FROM node:22-alpine AS builder
WORKDIR /app

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

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

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

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

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

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

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

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

Web (Next.js 16) template

FROM node:22-alpine AS builder
WORKDIR /app

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

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

COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
RUN npx next build --webpack

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

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

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

Docker build command

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

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

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

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

Registry configuration

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

Publish workflow

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

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

For full details, see GITEA_NPM_REGISTRY_MIGRATION.md.

Gitea 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. @bytelyst/* Package Distribution for Docker Builds

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

Status: Resolved. See GITEA_NPM_REGISTRY_MIGRATION.md for details.

F3. NomGap Backend Dockerfile

Status: Fixed. Uses Gitea registry pattern.

F4. NoteLett Backend Dockerfile

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

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

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

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

F6. Build Context Mismatch for ActionTrail + LocalMemGPT

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

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

F7. Node.js Version Inconsistency

Existing Dockerfiles use mixed Node versions:

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

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

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

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

F9. Missing .env.ecosystem Template

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

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

Status: Implemented as .env.ecosystem in the workspace root.

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

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

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

Summary of Audit Items

All audit findings have been resolved:

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

13. Kubernetes Roadmap Reference

Kubernetes planning has been split into a standalone roadmap:

  • docs/devops/KUBERNETES_ROADMAP.md

Use that document for:

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

SINGLE_VM_DEPLOYMENT.md remains the source of truth for:

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

Summary

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