Kubernetes
The same image that powers genosyn install runs fine on Kubernetes. You trade the one-line installer for raw manifests — and you give up the genosyn upgrade and genosyn backup commands, which only know how to drive Docker on a single host.
Architecture
Genosyn is a single stateless container. Everything that needs to survive a restart is either in Postgres or under /app/data:
- Deployment, 1 replica. Per-employee credential directories and CLI working trees under
/app/dataare filesystem state that the running process mutates. Scaling out horizontally would need a coordination layer Genosyn doesn't have today — keepreplicas: 1. - PersistentVolumeClaim at
/app/data(ReadWriteOnce is fine). Holds employee creds, materialized git checkouts,.mcp.jsonfiles, and uploaded attachments. - External Postgres. SQLite works inside a pod but dies with the pod. Run Postgres in-cluster (a separate Helm chart, CloudNativePG, Zalando, …) or point at a managed instance.
- Secret with config overrides. Genosyn's config is a bundled TypeScript object; on Kubernetes you overlay it at runtime — see below.
- Service + Ingress. The container listens on
8471. Front it with whatever Ingress controller you already run.
Prerequisites
- Cluster
- Any conformant Kubernetes 1.27+. Managed EKS / GKE / AKS, k3s, or kind all work — the manifests below are vanilla.
- Postgres
- Reachable from the cluster. Genosyn runs every migration on boot, so an empty database is fine.
- StorageClass
- One that supports
ReadWriteOnce. The default class on every managed cluster qualifies. - Ingress
- nginx, Traefik, or your cloud's controller — anything that can route HTTPS to a ClusterIP Service.
Overriding config
App/config.ts is compiled into the image at build time, so the live process reads /app/dist/config.js. To change values without rebuilding, mount a ConfigMap over that path. The compiled shape mirrors the source exactly:
apiVersion: v1
kind: ConfigMap
metadata:
name: genosyn-config
namespace: genosyn
data:
config.js: |
export const config = {
dataDir: "/app/data",
db: {
driver: "postgres",
sqlitePath: "",
postgresUrl: process.env.GENOSYN_POSTGRES_URL,
},
port: 8471,
publicUrl: "https://genosyn.example.com",
sessionSecret: process.env.GENOSYN_SESSION_SECRET,
smtp: {
host: "smtp.example.com", port: 587, secure: false,
user: "apikey", pass: process.env.GENOSYN_SMTP_PASS,
from: "Genosyn <no-reply@example.com>",
},
integrations: {
google: { clientId: "", clientSecret: "" },
},
};Sensitive values go in a separate Secret:
apiVersion: v1
kind: Secret
metadata:
name: genosyn-secrets
namespace: genosyn
type: Opaque
stringData:
GENOSYN_POSTGRES_URL: postgresql://genosyn:****@postgres:5432/genosyn
GENOSYN_SESSION_SECRET: "<32+ random bytes>"
GENOSYN_SMTP_PASS: "<smtp password or api key>"PVC, Deployment, Service, Ingress
One file, four objects. Apply with kubectl apply -f:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: genosyn-data
namespace: genosyn
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: genosyn
namespace: genosyn
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels: { app: genosyn }
template:
metadata:
labels: { app: genosyn }
spec:
containers:
- name: app
image: ghcr.io/genosyn/app:latest
ports:
- containerPort: 8471
envFrom:
- secretRef:
name: genosyn-secrets
volumeMounts:
- name: data
mountPath: /app/data
- name: config
mountPath: /app/dist/config.js
subPath: config.js
readOnly: true
readinessProbe:
httpGet: { path: /api/health, port: 8471 }
initialDelaySeconds: 10
livenessProbe:
httpGet: { path: /api/health, port: 8471 }
initialDelaySeconds: 30
volumes:
- name: data
persistentVolumeClaim:
claimName: genosyn-data
- name: config
configMap:
name: genosyn-config
---
apiVersion: v1
kind: Service
metadata:
name: genosyn
namespace: genosyn
spec:
selector: { app: genosyn }
ports:
- port: 80
targetPort: 8471
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: genosyn
namespace: genosyn
spec:
ingressClassName: nginx
rules:
- host: genosyn.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: genosyn
port: { number: 80 }Recreate over RollingUpdate because an RWO volume can only attach to one pod at a time. The old pod must terminate before the new one schedules.
Upgrading
The genosyn upgrade CLI command drives Docker on a single host — it has no idea about your cluster. Roll the Deployment instead:
kubectl -n genosyn set image deploy/genosyn app=ghcr.io/genosyn/app:0.3.47
kubectl -n genosyn rollout status deploy/genosynPin a tag rather than tracking latest — that's how you get repeatable rollbacks. Image tags carry no v prefix, even though the matching GitHub release does: the release is v0.3.47, the image is app:0.3.47.
Backups
On Docker, genosyn backup tarballs the data volume. On Kubernetes you back up two things, separately:
- The Postgres database. Use the backup story that shipped with your Postgres operator or managed service —
pg_dumpon a CronJob is the cheapest option. - The
genosyn-dataPVC. Use a VolumeSnapshot if your StorageClass supports it, or a CronJob thattars the volume to object storage.
Restore is symmetric: load Postgres first, then rehydrate the PVC, then start the Deployment.
A Helm chart?
Not officially shipped. The manifests above are short enough that templating them adds more friction than it removes for most teams — and a chart we'd have to lint, publish, and version across Genosyn releases is a real maintenance surface.
If you build one internally, the values worth parameterising are image.tag, ingress.host, persistence.size, and the contents of the config ConfigMap. Open an issue if you'd like to upstream it — community charts are welcome.
Next steps
Once the pod is healthy, open your Ingress host, create the first owner account, and follow the post-install path: pick a model, create an AI Employee, schedule a Routine.