Deploying Shoehorn with Helm
This guide covers deploying Shoehorn to a Kubernetes cluster using the official Helm chart.
Prerequisites
Section titled “Prerequisites”- Kubernetes cluster (v1.28+)
- Helm (v3.12+)
kubectlconfigured for your cluster- A storage class for persistent volumes
- DNS records pointing to your cluster’s ingress
Architecture
Section titled “Architecture”A production Shoehorn deployment consists of:
- API (2 replicas) - REST API gateway
- Web (2 replicas) - Svelte frontend
- Worker (3 replicas) - Background job processor
- Crawler (2 replicas) - GitHub repository discovery
- Forge (2 replicas) - Workflow engine
- EventBus (1 replica) - Event streaming manager
- PostgreSQL (1 replica or external managed)
- Meilisearch (1 replica)
- Valkey (1 replica or external managed)
- Redpanda (1 replica or external managed)
- Cerbos (1 replica) - Authorization engine
On 2-node clusters or nodes past 80% CPU-requested, set every replicaCount.* to 1. The default 2-replica services need a surge replica during rolling upgrade and can wedge against Insufficient cpu.
Step 1: Create Namespace and Secrets
Section titled “Step 1: Create Namespace and Secrets”kubectl create namespace shoehornDatabase Credentials
Section titled “Database Credentials”kubectl create secret generic database-credentials -n shoehorn \ --from-literal=postgres_password='<admin-password>' \ --from-literal=db_password='<app-user-password>'Authentication Credentials
Section titled “Authentication Credentials”kubectl create secret generic auth-credentials -n shoehorn \ --from-literal=session-encryption-key="$(openssl rand -hex 32)" \ --from-literal=service-user-pat='<zitadel-service-user-pat>'Service Credentials
Section titled “Service Credentials”kubectl create secret generic service-credentials -n shoehorn \ --from-literal=meilisearchMasterKey="$(openssl rand -base64 32)" \ --from-literal=valkey_password='<valkey-password>'GitHub Integration Credentials
Section titled “GitHub Integration Credentials”kubectl create secret generic integration-credentials -n shoehorn \ --from-literal=github_app_id='<app-id>' \ --from-literal=github_app_installation_id='<installation-id>' \ --from-file=github_app_private_key=path/to/private-key.pemSMTP Credentials (Optional)
Section titled “SMTP Credentials (Optional)”kubectl create secret generic smtp-credentials -n shoehorn \ --from-literal=smtp_password='<smtp-password>'Step 2: Create Values File
Section titled “Step 2: Create Values File”Create a values.yaml file for your deployment:
global: domain: shoehorn.example.com storageClass: "standard" # your cluster's storage class organization: name: "My Organization" slug: "my-org"
image: tag: "v0.7.0" # always pin; never use "latest" in production pullPolicy: IfNotPresent
# Service replicasreplicaCount: api: 2 web: 2 worker: 3 crawler: 2 forge: 2 eventbus: 1
# Authenticationauth: provider: zitadel # zitadel, okta, or entra-id zitadel: externalUrl: https://auth.example.com projectId: "<zitadel-project-id>" clientId: "<zitadel-client-id>"
# RBACrbac: enabled: true
# GitHub Integrationgithub: organizations: "my-org" manifestPatterns: ".shoehorn/**/*.yml,.shoehorn/**/*.yaml,catalog-info.yaml"
# Database (built-in)postgresql: enabled: true image: repository: shoehorned/shoehorn-postgres tag: "v18.3-pgaudit-1.0" # pinned, not tied to platform release persistence: size: 10Gi
# Searchmeilisearch: enabled: true persistence: size: 10Gi
# Cachevalkey: enabled: true
# Event Streamingredpanda: enabled: true
# Authorizationcerbos: enabled: true
# IngressingressRoute: enabled: true tls: enabled: true certResolver: letsencrypt
# Monitoring (optional)monitoring: enabled: falseStep 3: Install Ingress Controller
Section titled “Step 3: Install Ingress Controller”Shoehorn uses Traefik as the ingress controller:
helm repo add traefik https://traefik.github.io/chartshelm install traefik traefik/traefik \ --namespace traefik --create-namespace \ -f config/helm/prod-traefik-values.yamlStep 4: Install Shoehorn
Section titled “Step 4: Install Shoehorn”helm install shoehorn oci://ghcr.io/shoehorn-dev/helm-charts/shoehorn \ --namespace shoehorn \ -f values.yamlStep 5: Verify Deployment
Section titled “Step 5: Verify Deployment”# Check all pods are runningkubectl get pods -n shoehorn
# Check API healthkubectl port-forward -n shoehorn svc/api 8080:8080curl http://localhost:8080/healthStep 6: Configure DNS
Section titled “Step 6: Configure DNS”Point your domain to the Traefik load balancer IP:
kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}'Create DNS records:
| Record | Type | Value |
|---|---|---|
shoehorn.example.com | A | <load-balancer-ip> |
auth.example.com | A | <load-balancer-ip> |
Multi-Tenant RLS Architecture
Section titled “Multi-Tenant RLS Architecture”Shoehorn uses PostgreSQL Row-Level Security (RLS) for tenant isolation. All database tables have RLS policies that filter data by tenant_id. This is always enabled — there is no toggle.
How It Works
Section titled “How It Works”The Helm chart automatically:
- Runs a migration init container on the API deployment using
shoehorn_user(BYPASSRLS) to apply schema changes and create theapp_user - Configures all runtime services with
app_user(NOBYPASSRLS) in theirDATABASE_URL
Database Users
Section titled “Database Users”| User | RLS | Purpose |
|---|---|---|
shoehorn_user | BYPASSRLS | Schema migrations, creates app_user, admin operations |
app_user | NOBYPASSRLS | All runtime queries — RLS policies enforced by PostgreSQL |
Required Secret
Section titled “Required Secret”Your secret must contain two database passwords:
kubectl create secret generic database-credentials -n shoehorn \ --from-literal=postgres_password="$(openssl rand -base64 24)" \ --from-literal=db_password="$(openssl rand -base64 24)"postgres_password: used byshoehorn_userfor migrationsdb_password: used byapp_userfor runtime queries
For single-tenant deployments, RLS still runs but the middleware auto-injects a fixed tenant ID via global.organization.slug.
Connection Pool Tuning
Section titled “Connection Pool Tuning”See Connection Pool Tuning for sizing guidance.
Resource Recommendations
Section titled “Resource Recommendations”Small (up to 50 entities)
Section titled “Small (up to 50 entities)”resources: api: requests: { cpu: 100m, memory: 256Mi } limits: { cpu: 500m, memory: 512Mi } web: requests: { cpu: 50m, memory: 128Mi } limits: { cpu: 200m, memory: 256Mi } worker: requests: { cpu: 50m, memory: 128Mi } limits: { cpu: 200m, memory: 256Mi }Medium (50-500 entities)
Section titled “Medium (50-500 entities)”resources: api: requests: { cpu: 250m, memory: 512Mi } limits: { cpu: 1000m, memory: 1Gi } worker: requests: { cpu: 100m, memory: 256Mi } limits: { cpu: 500m, memory: 512Mi }Large (500+ entities)
Section titled “Large (500+ entities)”Enable autoscaling:
autoscaling: api: enabled: true minReplicas: 3 maxReplicas: 10 targetCPUUtilization: 70 worker: enabled: true minReplicas: 3 maxReplicas: 10Persistence
Section titled “Persistence”| Component | Default Size | Purpose |
|---|---|---|
| PostgreSQL | 10Gi | Primary database |
| Meilisearch | 10Gi | Search indexes |
| Valkey | - | In-memory cache (no persistence) |
| Redpanda | - | Event streaming (optional persistence) |
Uninstalling
Section titled “Uninstalling”helm uninstall shoehorn -n shoehornPVCs and the postgres StatefulSet survive the uninstall (helm.sh/resource-policy: keep). Delete them when you want the data gone:
kubectl delete sts -n shoehorn shoehorn-postgresqlkubectl delete pvc -n shoehorn --allkubectl delete namespace shoehorn