- Remove 'Supersedes' and 'What Changed' section from enhanced plan - Rewrite Package-Manager Strategy (transition complete, all repos on pnpm) - Remove docker-prep.sh prerequisites, .tarballs/ references, npm variants - Replace Dockerfile templates with current Gitea registry-backed pattern - Remove §11.1 Package-Manager Migration Roadmap (migration complete) - Clean up §11.2 Gitea section (remove 'Current pain', comparison table) - Clean up §12 audit findings (remove tarball references) - Simplify §10 Dockerization table (remove transition columns) - Update §5.1/5.2 to reflect validated state, not open gaps - Fix v2 tag in K3s exercise to use semver 1.1.0 - Update Summary table with current state
46 KiB
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_platis the canonicalpnpmworkspace monorepo for shared packages, services, and dashboards.- All 10 Node/TypeScript product repos use
pnpmwithpnpm-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
PORTenv var or remap viaports:mapping.
Optional / AI
| Service | Port | RAM Est. |
|---|---|---|
| Ollama (LLM) | 11434 | 4–16 GB (model-dependent) |
2. VM Sizing
Minimum (dev/staging, no Ollama)
| Spec | Value |
|---|---|
| vCPUs | 8 |
| RAM | 32 GB |
| Disk | 100 GB SSD |
| OS | Ubuntu 24.04 LTS |
Breakdown:
- Cosmos Emulator: ~2 GB
- 10 Fastify backends × 150 MB = ~1.5 GB
- 3 shared services × 250 MB = ~0.75 GB
- 11 Next.js webs × 200 MB = ~2.2 GB
- Infra (Traefik, Loki, Grafana, Azurite, Mailpit) = ~0.65 GB
- Gitea + act_runner = ~0.25 GB
- K3s overhead = ~0.5 GB
- Subtotal: ~7.65 GB → headroom for spikes + build cache = 32 GB
Recommended (with Ollama, small models)
| Spec | Value |
|---|---|
| vCPUs | 16 |
| RAM | 64 GB |
| Disk | 200 GB NVMe SSD |
| GPU | Optional NVIDIA T4/A10 for fast LLM inference |
| OS | Ubuntu 24.04 LTS |
Cloud Equivalents
| Provider | Instance | vCPU | RAM | Price (approx) |
|---|---|---|---|---|
| Azure | Standard_D8s_v5 | 8 | 32 GB | ~$280/mo |
| Azure | Standard_D16s_v5 | 16 | 64 GB | ~$560/mo |
| AWS | m6i.2xlarge | 8 | 32 GB | ~$280/mo |
| AWS | m6i.4xlarge | 16 | 64 GB | ~$560/mo |
| Hetzner | CPX51 | 16 | 32 GB | ~$45/mo |
| Hetzner | CCX63 | 48 | 192 GB | ~$230/mo |
| Home | Mac Mini M4 Pro | 12 | 48 GB | One-time ~$1,600 |
Cost tip: Hetzner is 5–10× cheaper than Azure/AWS for dev/staging.
3. Architecture: Docker Compose → K3s Migration Path
Phase 1: Docker Compose
📋 Enhanced plan: See
SINGLE_VM_ENHANCED_PLAN.mdfor the deployment plan with Coolify, Valkey, Uptime Kuma, and other open-source tooling.
Create a unified docker-compose.ecosystem.yml that brings everything up.
Phase 2: Local Kubernetes (Docker Desktop or K3s)
Two options for single-node K8s — both give you real kubectl, Helm, Ingress, and CRDs identical to production AKS/EKS/GKE.
Option A: Docker Desktop Kubernetes (recommended for Mac/Windows dev)
Docker Desktop includes a built-in kind (Kubernetes IN Docker) cluster. Enable it in Docker Desktop → Settings → Kubernetes → Enable Kubernetes.
- Zero install — checkbox in Docker Desktop, K8s v1.31+ included
- Images shared —
docker buildimages are immediately available to K8s (no import step!) - GUI dashboard — Docker Desktop shows Deployments, Pods, Services, Ingresses, ConfigMaps, Secrets
- kubectl pre-configured — context
docker-desktopauto-created - Helm works — install via
brew install helm - Best for: Mac/Windows local development, quick iteration, visual debugging
- Limitation: Single-node only, can't add workers (use K3s for multi-node practice)
Option B: K3s (recommended for Linux VMs / multi-node practice)
K3s 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 standardpnpm installwith 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 installagainst 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 -
8. Recommended Directory Structure
~/code/mygh/
├── docker-compose.ecosystem.yml # Phase 1: all-in-one compose
├── .env.ecosystem # Shared env vars
├── k8s/ # Phase 2: K3s manifests
│ ├── kustomization.yaml # Kustomize root
│ ├── infra/ # Cosmos emulator, Azurite, Mailpit, Loki, Grafana
│ ├── platform/ # platform-service, extraction, mcp
│ ├── products/ # 10 product backends
│ ├── web/ # 10+ Next.js dashboards
│ ├── config/ # ConfigMaps
│ └── secrets/ # Secrets (gitignored)
├── helm/ # Phase 3: Helm chart
│ └── bytelyst-ecosystem/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
└── scripts/
├── ecosystem-up.sh # docker compose -f docker-compose.ecosystem.yml up -d
├── ecosystem-k3s-deploy.sh # kubectl apply -k k8s/
└── ecosystem-build-all.sh # Build all Docker images
9. Quick Start Commands
# ── 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:
- Git hosting — mirror or primary for all 13+ repos
- CI/CD — GitHub Actions–compatible workflows via
act_runner - npm package registry — 49
@bytelyst/*packages published, consumed by all 10 product repos
Registry configuration
# .npmrc (host-side) — points @bytelyst scope to Gitea registry
@bytelyst:registry=http://localhost:3300/api/packages/bytelyst/npm/
//localhost:3300/api/packages/bytelyst/npm/:_authToken=${GITEA_NPM_TOKEN}
# .npmrc.docker (Docker-side) — uses compose service name or build arg
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/bytelyst/npm/
//localhost: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/publish-local-gitea-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 Actions–compatible workflows. |
| Package registry? | Gitea npm registry — 49 @bytelyst/* packages published. Also has Docker container registry. |
| K8s practice on single VM? | Docker Desktop K8s (Mac/Windows) or K3s (Linux). Same manifests scale to AKS/EKS/GKE. |
| Recommended VM? | 8 vCPU / 32 GB (min) or 16 vCPU / 64 GB (with Ollama). Hetzner ~€45/mo for dev. |
| Enhanced tooling? | See SINGLE_VM_ENHANCED_PLAN.md — 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. |