A web application for configuring, storing, and managing multi-service Helm-based application stacks for deployment to one or more Kubernetes clusters.
Developers create stack definitions (collections of Helm charts with configuration), launch stack instances (per-developer copies with branch and value overrides), and manage everything through an audit-logged UI with Git provider integration.
Deploy to your cluster and create your first stack in under 10 minutes:
# Install the CLI
brew install omattsson/tap/stackctl
# Add the Helm repo
helm repo add k8s-stack-manager https://omattsson.github.io/k8s-stack-manager
helm repo update
# Deploy to your cluster (see docs/getting-started.md for secrets setup)
helm install stack-manager k8s-stack-manager/k8s-stack-manager \
--namespace stack-manager --create-namespace \
--values stack-manager-values.yamlSee the full Getting Started Guide for next steps (configure stackctl, register a cluster, import starter templates, and deploy your first stack).
Don't need the UI? Skip straight to the Headless / API-only deployment section — stackctl drives every workflow.
View, search, and manage all your stack instances from a single dashboard. Filter by status (draft, deploying, running, stopped, error), star your favorites for quick access, and see recently used stacks at a glance. Bulk operations let you deploy, stop, clean, or delete up to 50 instances at once.
Browse and discover reusable stack templates organized by category (Web, API, Data, Infrastructure). Each template includes a description, version tag, and one-click Quick Deploy to spin up a new instance instantly. Create your own templates and publish them for your team.
Define multi-chart application stacks with Helm chart configurations, default branches, and value templates. Import and export definitions as JSON bundles for sharing across environments. Each definition can be used to create templates or directly instantiate stack instances.
Track platform usage with real-time metrics: template and definition counts, running instances, total deployments, and active users. The template usage table shows deployment success rates and adoption across the team.
Full audit trail of every action in the system — creates, updates, deletes — with filters by user, entity type, action, and date range. Export logs for compliance. Every mutating API call is automatically logged with user identity and entity details.
Manage your account, generate API keys for CI/CD automation, and configure notification preferences per event type (deployment succeeded/failed, stopped, deleted).
- Multi-cluster support — Register and manage multiple Kubernetes clusters with encrypted kubeconfig storage (AES-GCM). Monitor cluster health and resource utilization.
- Git provider integration — Automatic branch listing from Azure DevOps and GitLab repositories, with per-chart branch overrides.
- Helm values deep merge — Chart defaults are deep-merged with instance overrides. Template variables (
{{.Branch}},{{.Namespace}},{{.InstanceName}}, etc.) are substituted automatically. - Cleanup policies — Schedule cron-based cleanup actions (stop, clean, delete) on instances matching custom conditions.
- TTL auto-expiry — Set time-to-live on instances; a background reaper automatically stops expired deployments.
- Real-time updates — WebSocket-based live updates push deployment status changes to all connected clients.
- RBAC — Role-based access control (admin, devops, developer) with JWT authentication and optional OpenID Connect (OIDC) SSO.
- In-app notifications — Get notified on deploy/stop/clean events with configurable per-user preferences.
- Instance comparison — Side-by-side diff of two stack instances including merged Helm values per chart.
- Shared values — Per-cluster shared Helm values applied to all instances, merged by priority before instance-specific overrides.
- Dark mode — Full dark/light theme support.
k8s-stack-manager is deliberately free of organisation-specific logic. Database refreshes, CMDB sync, policy gates, custom snapshots, Slack notifications — all of this can be added without forking, in any language, as a small out-of-process service.
Two mechanisms:
- Event hooks — POST handlers subscribe to lifecycle events (
pre-deploy,post-instance-create, …).failure_policy: failsubscribers can abort operations to enforce policy. - Actions —
POST /api/v1/stack-instances/:id/actions/:namedispatches to a subscriber you register. Subscriber responses are forwarded verbatim to callers — so stackctl can expose them as first-class subcommands via plugin discovery.
Both run over plain HTTP with HMAC signing. Build a subscriber in 10 minutes using Python stdlib (see backend/examples/webhook-handler-python/) or in Go (backend/examples/webhook-handler/).
👉 Full guide: EXTENDING.md — tutorial, event reference, action contract, security, real-world recipes.
Frontend (React + MUI + TypeScript)
│
▼
Backend (Go + Gin)
├── REST API with JWT auth
├── MySQL (GORM)
├── Git Provider (Azure DevOps + GitLab)
├── Helm Values (deep merge + template substitution)
├── Multi-cluster support (kubeconfig encrypted at rest)
└── Audit Logging
- Docker and Docker Compose
- Go 1.26+ (for local backend development)
- Node.js 22+ (for local frontend development)
cp .env.example .env # first time only — sets COMPOSE_PROFILES=full
make devThis starts all services:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8081
- Swagger docs: http://localhost:8081/swagger/index.html
Default admin credentials: admin / admin (configured in docker-compose.yml).
# Start MySQL (dev-local-backend does NOT do this on its own; the
# combined dev-local target does).
make mysql-start
# Run backend only — `make dev-local` already starts both backend AND
# frontend in one terminal; use `dev-local-backend` if you'd rather run
# them in separate terminals (e.g. to tail logs per service).
make dev-local-backend
# In another terminal — run frontend
cd frontend && npm install && npm run devk8s-stack-manager runs perfectly well without the React UI — every workflow the dashboard exposes is also driven by the REST API, and stackctl is the supported headless client. The pieces below are independent; pick whichever matches your runtime.
- CI/CD pipelines that drive deploys/stops/rollbacks with
stackctl - Air-gapped environments where shipping a SPA isn't worth the surface area
- Reduced resource footprint (one less Deployment, no nginx)
- Faster cold starts in ephemeral preview environments
The frontend never authenticates against the backend differently from the CLI, so dropping it costs you nothing operationally.
# One-shot — Makefile target sets the profile for you
make compose-api-only
# Or explicitly via env var (works with any compose subcommand)
COMPOSE_PROFILES=api-only docker compose updocker-compose.yml tags the frontend service with profiles: [full], so it
only runs when that profile is active. .env.example sets
COMPOSE_PROFILES=full by default; drop or override it to go headless.
Upgrading from a previous checkout? If you have an existing
.envfrom before this change, addCOMPOSE_PROFILES=fullto it (orcp .env.example .envafresh) — otherwisedocker compose upwill now start the headless stack.make dev/make prodforce--profile fulland are unaffected.
The chart's frontend.enabled toggle skips every frontend resource
(Deployment/Rollout, Service, ConfigMap, HPA, PDB, ServiceAccount) and the
/ ingress rule. The backend routes (/api, /ws, /health, /swagger)
are unaffected.
# Install from the published Helm repo (matches the "Getting Started"
# example above). Use the local chart path `helm/k8s-stack-manager` only
# when you have a repo checkout — typically for chart development.
helm install stack-manager k8s-stack-manager/k8s-stack-manager \
--namespace stack-manager --create-namespace \
--set backend.secrets.JWT_SECRET=my-secret-at-least-16-chars \
--set frontend.enabled=false \
--set ingress.host=stacks.example.comOnce the API is reachable, install stackctl and point it at the cluster:
# Install
brew install omattsson/tap/stackctl
# Point at the backend (admin credentials from your install)
stackctl config set api-url https://stacks.example.com
stackctl login # username/password
# …or, if your install has OIDC configured:
stackctl login --sso # opens a browser, RFC 8252 loopback flowstackctl login --sso uses the OIDC loopback flow — the CLI calls the
backend's cli-auth endpoint, opens the returned login_url in your
default browser, and starts a local HTTP server on 127.0.0.1:<random-port>.
After you authenticate with the upstream IdP, the backend 302-redirects the
browser to the CLI's local server with the tokens in the query string. No
copy/paste, no polling, no frontend involved.
Once authenticated, every API operation has a first-class CLI surface:
stackctl template list
stackctl stack deploy my-app
stackctl stack watch --id <instance-id> # real-time WS events
stackctl audit log export --format csv --output-file audit.csvSee the stackctl README for the full command surface.
| Command | Description |
|---|---|
make dev |
Start full stack via Docker Compose |
make compose-api-only |
Start backend + mysql only (headless, no frontend) |
make dev-local |
Run backend + frontend locally |
make test |
Run all tests (backend + frontend) |
make test-backend |
Backend unit tests |
make test-frontend |
Frontend unit tests |
make test-backend-all |
Backend unit + integration tests |
make test-e2e |
End-to-end Playwright tests |
make docs |
Regenerate Swagger documentation |
make lint |
Lint backend + frontend |
make clean |
Stop containers and remove volumes |
make install |
Install all dependencies |
make helm-lint |
Lint the Helm chart |
make helm-template |
Render templates locally (dry-run) |
make helm-install |
Install chart into current cluster |
make helm-upgrade |
Upgrade an existing release |
make helm-uninstall |
Uninstall the release |
├── backend/ # Go API server
│ ├── api/main.go # Application entry point
│ ├── internal/
│ │ ├── api/ # Handlers, middleware, routes
│ │ ├── config/ # Environment-based configuration
│ │ ├── database/ # Database repositories + migrations
│ │ ├── gitprovider/ # Azure DevOps + GitLab integration
│ │ ├── helm/ # Values merge + template substitution
│ │ ├── cluster/ # Multi-cluster registry + health poller
│ │ ├── deployer/ # Helm CLI wrapper for deploy/undeploy (multi-cluster)
│ │ ├── k8s/ # Cluster client + status monitoring
│ │ ├── models/ # Domain models + interfaces
│ │ ├── scheduler/ # Cron-based cleanup policy execution
│ │ ├── ttl/ # TTL reaper for auto-expiring stack instances
│ │ └── websocket/ # Real-time event broadcasting
│ └── pkg/crypto/ # AES-GCM encryption for kubeconfig at rest (key derived via SHA-256)
│ └── docs/ # Swagger/OpenAPI
├── frontend/ # React SPA
│ └── src/
│ ├── api/ # API client + types
│ ├── components/ # Shared UI components
│ ├── context/ # Auth + WebSocket contexts
│ ├── pages/ # Page components
│ └── routes.tsx # Route definitions
├── helm/k8s-stack-manager/ # Helm chart (Argo Rollouts + Traefik)
│ ├── templates/backend/ # Backend Rollout, services, config
│ ├── templates/frontend/ # Frontend Rollout, services, nginx config
│ └── templates/traefik/ # IngressRoute + middleware
├── loadtest/ # Load testing suites
│ ├── backend/ # k6 API + WebSocket load tests
│ └── frontend/ # Playwright load tests
└── docker-compose.yml
| Group | Prefix | Description |
|---|---|---|
| Auth | /api/v1/auth |
Login, register, current user |
| Templates | /api/v1/templates |
Stack template CRUD, publish, instantiate |
| Definitions | /api/v1/stack-definitions |
Stack definition CRUD, chart configs |
| Instances | /api/v1/stack-instances |
Stack instance CRUD, clone, deploy, stop, clean, status |
| Overrides | /api/v1/stack-instances/:id/overrides |
Per-chart value overrides |
| Branch Overrides | /api/v1/stack-instances/:id/branches |
Per-chart branch overrides |
| Git | /api/v1/git |
Branch listing, validation |
| Audit Logs | /api/v1/audit-logs |
Filterable audit trail + export |
| Admin | /api/v1/admin |
Orphaned namespace detection and cleanup |
| Clusters | /api/v1/clusters |
Multi-cluster registration, health, test-connection |
| Shared Values | /api/v1/clusters/:id/shared-values |
Per-cluster shared Helm values |
| Cleanup Policies | /api/v1/admin/cleanup-policies |
Cron-based cleanup policy management |
| Analytics | /api/v1/analytics |
Usage overview, template stats, user stats |
| Favorites | /api/v1/favorites |
User bookmark management |
| Quick Deploy | /api/v1/templates/:id/quick-deploy |
One-click template deployment |
| Health | /health/* |
Liveness + readiness |
Key environment variables (see docker-compose.yml for full list):
| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | JWT signing secret (min 16 chars) |
ADMIN_PASSWORD |
Yes | Initial admin password |
AZURE_DEVOPS_PAT |
No | Azure DevOps personal access token |
GITLAB_TOKEN |
No | GitLab access token |
DEFAULT_BRANCH |
No | Default Git branch (default: master) |
KUBECONFIG_ENCRYPTION_KEY |
No | Passphrase for deriving AES-256 key (SHA-256) to encrypt kubeconfig data at rest |
SESSION_STORE |
No | Session store backend: mysql (default) or memory |
The Helm chart in helm/k8s-stack-manager/ deploys the full stack to Kubernetes using Argo Rollouts (canary strategy) and Traefik IngressRoute.
- Kubernetes cluster with
kubectlcontext configured - Helm 3+
- Argo Rollouts controller installed
- Traefik ingress controller with CRDs
Note: The default chart configuration requires
backend.secrets.JWT_SECRETat render time. You must provide this value via--setor theJWT_SECRETenv var before running lint/install.
# Lint the chart
make helm-lint
# Install (JWT_SECRET env var required)
JWT_SECRET=my-secret-at-least-16-chars make helm-install
# Or install with custom values directly
helm install k8s-stack-manager helm/k8s-stack-manager \
--namespace k8s-stack-manager --create-namespace \
--set backend.secrets.JWT_SECRET=my-secret-at-least-16-chars \
--set ingress.host=stacks.example.com
# Upgrade after changes
JWT_SECRET=my-secret-at-least-16-chars make helm-upgrade
# Uninstall
make helm-uninstallSet externalSecrets.enabled=true to have External Secrets Operator (ESO) create the backend Secret from Azure Key Vault. In this mode the chart does not render its inline backend Secret; ESO creates the same <fullname>-backend Secret consumed by the backend workload. When bundled MySQL is enabled, ESO also creates the MySQL Secret with MYSQL_ROOT_PASSWORD. If mysql.auth.user is non-empty, it also requires MYSQL_PASSWORD; both keys are mapped only to the MySQL Secret. The backend DB_PASSWORD is derived from the selected MySQL credential mapping.
Before installation, install ESO with its external-secrets.io/v1 CRDs, grant the Azure identity read access to Key Vault secrets, and configure AKS workload identity. For the chart-created SecretStore, create a federated identity credential for the managed identity using subject system:serviceaccount:<namespace>:<fullname>-backend, where <fullname> is the chart's rendered fullname. For a normal stack-manager release this is stack-manager-k8s-stack-manager-backend; when the release name already contains k8s-stack-manager, the chart uses the release name directly. fullnameOverride sets <fullname> to that override. The chart annotates that ServiceAccount with azure.workload.identity/client-id.
# external-secrets-values.yaml
externalSecrets:
enabled: true
azureKeyVault:
vaultUrl: https://my-vault.vault.azure.net
tenantId: 00000000-0000-0000-0000-000000000000
authType: WorkloadIdentity
workloadIdentity:
clientId: 00000000-0000-0000-0000-000000000000
data:
- secretKey: JWT_SECRET
remoteRef:
key: k8s-stack-manager-jwt-secret
- secretKey: ADMIN_PASSWORD
remoteRef:
key: k8s-stack-manager-admin-password
- secretKey: MYSQL_ROOT_PASSWORD
remoteRef:
key: k8s-stack-manager-mysql-root-password
- secretKey: MYSQL_PASSWORD
remoteRef:
key: k8s-stack-manager-mysql-passwordThe mapping above applies when mysql.enabled=true. In that case, do not add an
independent DB_PASSWORD entry: the backend Secret derives DB_PASSWORD from
the selected MYSQL_PASSWORD or MYSQL_ROOT_PASSWORD mapping. When
mysql.enabled=false, map the external database password explicitly, including
its Key Vault remoteRef:
externalSecrets:
data:
- secretKey: DB_PASSWORD
remoteRef:
key: k8s-stack-manager-external-db-passwordhelm upgrade --install k8s-stack-manager helm/k8s-stack-manager \
--namespace k8s-stack-manager --create-namespace \
--values external-secrets-values.yamlexternalSecrets.data explicitly maps every Key Vault secret to an environment key. Include every non-empty key required by backend.secrets, plus MYSQL_ROOT_PASSWORD when mysql.enabled=true; also include MYSQL_PASSWORD when mysql.auth.user is non-empty. Do not map DB_PASSWORD independently: the backend Secret derives it from MYSQL_PASSWORD when mysql.auth.user is set, otherwise from MYSQL_ROOT_PASSWORD. The MySQL password mappings target the MySQL Secret, not the backend Secret. Do not place Key Vault values or Azure credentials in the values file.
This MySQL password guidance applies only when mysql.enabled=true. When mysql.enabled=false, include a DB_PASSWORD mapping with the remoteRef for the external database password.
For a centrally managed store, set externalSecrets.secretStore.create=false, externalSecrets.secretStore.kind=ClusterSecretStore, and externalSecrets.secretStore.name to its name. The chart then creates only the ExternalSecret; it does not configure Azure authentication. To use ServicePrincipal for a chart-created store, set authType: ServicePrincipal and point externalSecrets.azureKeyVault.servicePrincipal.secretName at an existing Kubernetes Secret containing the configured client-id and client-secret keys.
- Backend — Argo Rollout (canary 20%→50%→80%) with stable + canary services
- Frontend — Argo Rollout with nginx serving the React SPA
- Traefik IngressRoute — Routes
/api/*→backend,/ws→backend,/→frontend - Traefik Middleware — StripPrefix for
/api, secure response headers
See helm/k8s-stack-manager/values.yaml for all configurable values.
make test # All unit tests
make test-backend-all # Backend unit + integration
make test-e2e # Playwright end-to-end
cd backend && make test-coverage # Coverage report (80% threshold)See LICENSE.





