Terraform module
For teams managing infra as code. One terraform apply deploys the platform and optionally the K8s agent.
Three ways to install. Pick one. All three end at the same place: a running platform on your cluster.
Terraform module
For teams managing infra as code. One terraform apply deploys the platform and optionally the K8s agent.
Helm chart
For teams with existing Kubernetes tooling. You bring the secrets and the values file.
Managed
Talk to a cloud partner. They run the install and the upgrades.
Use the shoehorn-dev/terraform-shoehorn-modules repo. Pin the module to a release tag.
Create a main.tf:
terraform { required_version = ">= 1.5.0" required_providers { helm = { source = "hashicorp/helm", version = ">= 3.0.0" } kubernetes = { source = "hashicorp/kubernetes", version = ">= 2.35.0" } random = { source = "hashicorp/random", version = ">= 3.0.0" } }}
provider "kubernetes" { config_path = "~/.kube/config" }provider "helm" { kubernetes = { config_path = "~/.kube/config" } }
resource "random_password" "pg_admin" { length = 32, special = false }resource "random_password" "pg_app" { length = 32, special = false }resource "random_password" "jwt" { length = 64, special = false }resource "random_bytes" "auth_enc_key" { length = 32 }resource "random_bytes" "session_key" { length = 32 }resource "random_password" "valkey" { length = 32, special = false }resource "random_password" "meili" { length = 32, special = false }
module "shoehorn" { source = "github.com/shoehorn-dev/terraform-shoehorn-modules//modules/kubernetes?ref=v0.7.0"
domain = "shoehorn.acme.com" organization_name = "Acme Corp" organization_slug = "acme-corp" admin_email = "platform@acme.com"
auth_provider = "okta" auth_config = { domain = "acme.okta.com" clientId = "0oa..." issuer = "https://acme.okta.com/oauth2/default" }
credentials = { postgres_password = random_password.pg_admin.result db_password = random_password.pg_app.result jwt_secret = random_password.jwt.result auth_encryption_key = random_bytes.auth_enc_key.base64 session_encryption_key = random_bytes.session_key.base64 valkey_password = random_password.valkey.result meilisearch_master_key = random_password.meili.result okta_client_secret = var.okta_client_secret }}
variable "okta_client_secret" { type = string sensitive = true}
output "url" { value = module.shoehorn.url }Then:
terraform initterraform applyThe module creates the namespace, every Kubernetes Secret the chart needs, and runs the Helm release. To deploy the K8s agent in the same apply, set deploy_agent = true and pass cluster_id / cluster_name.
Full reference, examples (Okta-only, Okta + agent, Okta + GitHub App + Forge), and gotchas: terraform-shoehorn-modules.
The chart lives on Artifact Hub: artifacthub.io/packages/helm/shoehorn/shoehorn. Source and the canonical values examples: shoehorn-dev/helm-charts.
The chart never creates Secrets. Provide one Secret with every credential as a key, then point the chart at it via secret.defaultName.
kubectl create namespace shoehorn
kubectl create secret generic shoehorn-credentials -n shoehorn \ --from-literal=postgres_password="$(openssl rand -base64 24)" \ --from-literal=db_password="$(openssl rand -base64 24)" \ --from-literal=valkey_password="$(openssl rand -base64 24)" \ --from-literal=meilisearch_master_key="$(openssl rand -hex 32)" \ --from-literal=jwt_secret="$(openssl rand -hex 32)" \ --from-literal=auth_encryption_key="$(openssl rand -hex 32)" \ --from-literal=secrets_encryption_key="$(openssl rand -hex 32)" \ --from-file=github_app_private_key=path/to/github-app-private-key.pemAdd your IdP client secret to the same Secret (okta_client_secret, or the Zitadel/Entra equivalent). See the Helm chart reference for every credential the chart can consume.
Don’t hand-write the values file from scratch. The chart ships working starting points:
| Example | Use when |
|---|---|
values-minimal.yaml | Local cluster or first test install |
values-okta.yaml | Okta auth |
values-production.yaml | Production sizing, ingress, TLS |
values-production-tls.yaml | Production with internal gRPC mTLS |
values-external-db.yaml | External managed Postgres / Valkey |
values-eso-vault.yaml | External Secrets Operator + Vault |
Copy the one closest to your setup and edit. Things you’ll always need to fill in:
global.domain and global.organization.slugauth.provider and the matching provider block (auth.okta.*, auth.zitadel.*, or auth.entraId.*), including the clientSecretRefrbac.roleAssignment.tenantAdmin.user (a single email) or .group (a single IdP group): sets the bootstrap admin on first sign-in. Without it, no one can reach the role-management UI after install.auth.github.appId and auth.github.installationId (public identifiers)*SecretRef keys for every credential in your Secrethelm install shoehorn oci://ghcr.io/shoehorn-dev/helm-charts/shoehorn \ --namespace shoehorn \ --values values.yaml \ --waitThe chart fails template rendering when a required *SecretRef can’t resolve or when the auth provider is missing required fields. Errors surface as plain messages in the helm install output.
For the full values reference, sizing guidance, RLS internals, persistence settings, and uninstall steps: Helm chart reference.
Cloud partners could ship a turnkey Shoehorn deployment: run the cluster, the install, the upgrades, and the on-call. You’d configure it via the Shoehorn Terraform provider once it’s running.
No managed partners are listed yet. If you want a managed install today, the same Terraform module a partner would use is public: terraform-shoehorn-modules. Switch to the Terraform tab above.
Whichever path you took, the pods should be Running within a few minutes:
kubectl get pods -n shoehornkubectl port-forward -n shoehorn svc/shoehorn-api 8080:8080curl http://localhost:8080/healthz