Skip to content

🎃 Add a Helm chart for self-hosted Kubernetes installs #1007

Open

Description

@helpcodeai

Part of the October Challenge #997.

Why

Teams that run everything on Kubernetes cannot use docker compose up. Today they translate docker-compose.yml into manifests themselves and have to work out which paths go to which port, what the secrets must look like and how long the app needs to boot. A Helm chart is the usual way to install a self-hosted service on Kubernetes (n8n, Grafana and Keycloak ship one). The repository has no Helm or Kubernetes files, and the docs do not mention Kubernetes.

Self-hosted vs Cloud

  • Self-hosted: a new install method for self-hosted instances, Community and Business alike. Licence and edition logic is unchanged and the chart says nothing about editions. The chart does not set DEPLOYMENT_MODE, so instances stay self-hosted (docs/deployment.md:353).
  • Cloud (DEPLOYMENT_MODE=cloud): not affected. AnythingMCP Cloud runs on Docker Compose with a blue/green release script (docker-compose.cloud.yml, deploy/cloud/release.sh), not on Kubernetes. Do not change anything under deploy/cloud/ or the compose files.
  • Who can use it: whoever operates the cluster. App roles are untouched: on a fresh install the first account registered becomes the admin of a new organization (auth.controller.ts:490).

What to build

Chart at deploy/helm/anythingmcp/ with Chart.yaml (apiVersion v2, appVersion = the root package.json version, chart version starting at 0.1.0), a commented values.yaml, values.schema.json, templates/ and a README.md. No subchart dependencies.

  1. App Deployment:
    • Image helpcodeai/anythingmcp, tag defaulting to appVersion. Check the version tag format published on Docker Hub and never default to latest. The image is linux/amd64 only (docker-publish.yml:80); say so in the README.
    • Default command (./start.sh, both processes in one pod; migrations run at start). Container ports 3000 (UI) and 4000 (API/MCP), one Service with both.
    • replicaCount: 1. Document that more replicas need Redis (REDIS_URL) for shared rate limits, and sticky sessions if MCP_STATEFUL_SESSIONS is enabled, because stateful MCP sessions live in process memory (mcp-servers/mcp-session.manager.ts).
    • Probes: a startupProbe on GET /health:4000 that allows at least 180 s (boot loads the whole catalog; the Dockerfile uses a 120 s start period, Dockerfile:168-178); readiness on /health; liveness on a TCP check of port 4000, because /health also checks the database (health.controller.ts:74-80) and a database outage should not restart the pod.
    • securityContext: runAsNonRoot, uid/gid 1001 (Dockerfile:125, :164), allowPrivilegeEscalation: false, capabilities dropped; an emptyDir at /app/diagnostics (HEAP_SNAPSHOT_DIR, Dockerfile:154). If you can make readOnlyRootFilesystem: true work with a few emptyDir mounts, do it and list the paths; otherwise say what blocked it.
    • Resources with defaults, and NODE_MAX_OLD_SPACE_MB (read by start.sh) set below the memory limit.
  2. Configuration: NODE_ENV=production, PORT=4000, and SERVER_URL, FRONTEND_URL, CORS_ORIGIN, NEXT_PUBLIC_API_URL all derived from one publicUrl value (CORS_ORIGIN is mandatory in production, main.ts:215-235). MCP_AUTH_MODE (default legacy, as in docker-compose.yml), MCP_ALLOW_ANONYMOUS=false, ALLOW_OPEN_REGISTRATION=false, optional REDIS_URL, plus extraEnv / extraEnvFrom for everything else (SMTP, KG LLM, SSO).
  3. Secrets: JWT_SECRET, ENCRYPTION_KEY, MCP_API_KEY and the database password, either from existingSecret or generated by the chart. Generated values must survive helm upgrade (read the existing Secret with lookup, annotate helm.sh/resource-policy: keep): a new ENCRYPTION_KEY makes every stored connector credential unreadable. At least 32 characters; the backend refuses placeholder-looking values (common/secrets.util.ts:48-66), so values.yaml must not ship example secrets.
  4. Database: postgresql.enabled: true (default) deploys one postgres:17-alpine StatefulSet with a PVC, for evaluation and small installs. externalDatabase.url or externalDatabase.existingSecret for production, which disables the bundled one.
  5. Ingress (disabled by default): one host; the backend paths to port 4000 and everything else to 3000. Use the same list as the Cloud Caddyfile (deploy/cloud/Caddyfile:88-123): /api, /health, /mcp, /.well-known, /auth, /sso, /authorize, /callback, /token, /register, /userinfo. Explicit routing matters: the frontend's own rewrites to the backend are baked in at build time to http://localhost:4000 (packages/frontend/next.config.ts:5, note in docker-compose.cloud.yml:209-214). ingressClassName, TLS and annotations come from values; document for ingress-nginx proxy-buffering: "off" and a read timeout of at least 120 s, because MCP responses can be streamed and tool calls can take a while.
  6. NOTES.txt: the URL, "open it and register the first user", and a reminder to back up ENCRYPTION_KEY together with the database (docs/operations/backup-restore.md).
  7. CI: a job in .github/workflows/ci.yml that runs helm lint, and helm template piped to kubeconform -strict for the default values and for an external-database + ingress values file. Changes under .github/ get extra review, so keep the workflow diff small.
  8. Docs: a "Kubernetes (Helm)" section in docs/deployment.md after "Production Deployment" (:159), linking the chart README.

Where to look

  • docker-compose.yml: the app's environment variables and defaults.
  • start.sh: all / backend / frontend modes, migrations at start, NODE_MAX_OLD_SPACE_MB.
  • Dockerfile:108-181: runtime user, ports, diagnostics directory, healthcheck and why its start period is long.
  • docs/deployment.md:202-246 (path routing behind a proxy), :338-363 (environment variables).
  • deploy/cloud/Caddyfile:84-123: complete list of backend-owned paths.
  • docker-compose.cloud.yml:203-216: why the frontend cannot proxy to a separate backend.
  • packages/backend/src/common/secrets.util.ts: secret validation at startup.
  • packages/backend/src/health/health.controller.ts:74-80: what /health checks.
  • packages/backend/src/mcp-servers/mcp-session.manager.ts: in-memory stateful sessions.

Acceptance criteria

  • helm lint passes; helm template output for both values files passes kubeconform -strict.
  • On kind or minikube: helm install → pod Ready → UI reachable → first user registered → a connector created → tools/list on /mcp/<serverId> answers with an MCP API key (commands and output in the PR, secrets redacted).
  • helm upgrade without value changes keeps JWT_SECRET and ENCRYPTION_KEY: after the upgrade, existing sessions stay valid and connector credentials still decrypt.
  • The pod runs as non-root; no secret value appears in a ConfigMap or in values.yaml defaults.
  • The CI job is green; docs/deployment.md and the chart README are written.

Out of scope

  • Publishing the chart (Helm repository, OCI registry, Artifact Hub): maintainers decide after merge.
  • Separate Deployments for frontend and backend (./start.sh backend|frontend), autoscaling, multi-replica support.
  • Bundling Redis or the MOTIS service used by the Deutsche Bahn connector.
  • Operators, Kustomize, Terraform.
  • Any change to deploy/cloud/, the compose files or the Dockerfile.

Size

L (several days, may be split: app Deployment, Secret and external database first; bundled Postgres, Ingress and CI second).

How to claim

Comment "I'd like to work on this" and we'll assign you. Rules in #997.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthacktoberfestGood for Hacktoberfest contributorshelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions