Self-hosting

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/data are filesystem state that the running process mutates. Scaling out horizontally would need a coordination layer Genosyn doesn't have today — keep replicas: 1.
  • PersistentVolumeClaim at /app/data (ReadWriteOnce is fine). Holds employee creds, materialized git checkouts, .mcp.json files, 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:

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

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

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

bash
kubectl -n genosyn set image deploy/genosyn app=ghcr.io/genosyn/app:0.3.47
kubectl -n genosyn rollout status deploy/genosyn

Pin 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_dump on a CronJob is the cheapest option.
  • The genosyn-data PVC. Use a VolumeSnapshot if your StorageClass supports it, or a CronJob that tars 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.