Deploying Shoehorn with ArgoCD
Production-ready ArgoCD manifests for deploying the Shoehorn Helm chart without common anti-patterns.
Prerequisites
Section titled “Prerequisites”- ArgoCD v2.9+ installed in your cluster
kubectlaccess to the ArgoCD namespace- Shoehorn secrets already created (see Helm deployment guide)
- OCI registry credential configured in ArgoCD (type
helm,enableOCI: true)
Important: How ArgoCD Deploys Helm Charts
Section titled “Important: How ArgoCD Deploys Helm Charts”ArgoCD runs helm template and applies the output via kubectl apply. It never runs helm install or helm upgrade. This means:
- Helm hooks (
pre-install,post-install) are converted to ArgoCD sync hooks with different lifecycle semantics .Release.IsInstall/.Release.IsUpgradeare not meaningful- Use ArgoCD sync hooks (
PreSync,PostSync,SyncFail) instead of Helm hooks when needed
Step 1: Lock Down the Default AppProject
Section titled “Step 1: Lock Down the Default AppProject”Before creating anything, restrict the default project so Applications cannot escape tenant boundaries:
kubectl patch appproject default -n argocd --type merge -p '{"spec":{"sourceRepos":[],"destinations":[]}}'Step 2: Create the Shoehorn AppProject
Section titled “Step 2: Create the Shoehorn AppProject”Scope permissions to only what Shoehorn needs.
apiVersion: argoproj.io/v1alpha1kind: AppProjectmetadata: name: shoehorn namespace: argocdspec: description: Shoehorn Intelligent Developer Platform sourceRepos: # Explicit repo -- never use '*' which allows any source - "ghcr.io/shoehorn-dev/helm-charts" destinations: # Single namespace on the local cluster only - namespace: shoehorn server: https://kubernetes.default.svc clusterResourceWhitelist: - group: "" kind: Namespace - group: "cert-manager.io" kind: ClusterIssuer namespaceResourceBlacklist: # Secrets managed outside ArgoCD (pre-created or via External Secrets) - group: "" kind: SecretStep 3: Create the Application
Section titled “Step 3: Create the Application”apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: shoehorn namespace: argocd annotations: # Notify on sync status (if using argocd-notifications) notifications.argoproj.io/subscribe.on-sync-succeeded.slack: shoehorn-deploys notifications.argoproj.io/subscribe.on-sync-failed.slack: shoehorn-deploys # Scope manifest generation to this chart only (critical for monorepo perf) argocd.argoproj.io/manifest-generate-paths: . finalizers: # Ensures child resources are cleaned up on Application deletion. # CAUTION: Remove this finalizer BEFORE renaming or moving the Application, # otherwise ArgoCD cascade-deletes all managed resources. - resources-finalizer.argocd.argoproj.iospec: project: shoehorn
source: # OCI source -- no oci:// prefix (ArgoCD adds it automatically) repoURL: ghcr.io/shoehorn-dev/helm-charts chart: shoehorn # Always pin to a specific semver -- never use '*', 'latest', or mutable tags targetRevision: 0.1.0 helm: releaseName: shoehorn # Inline values -- for larger configs, use multi-source with a Git values repo values: | global: domain: shoehorn.example.com environment: production logLevel: info storageClass: "gp3" organization: name: "Acme Corp" slug: "acme-corp"
image: # Pin image tag to a release. Avoid 'latest' for reproducibility. tag: "v2026.3.0" pullPolicy: IfNotPresent
replicaCount: api: 2 web: 2 worker: 3 crawler: 2 forge: 2 eventbus: 1
auth: provider: zitadel zitadel: externalUrl: https://auth.example.com projectId: "CHANGE_ME" clientId: "CHANGE_ME"
postgresql: enabled: true persistence: size: 20Gi
meilisearch: enabled: true persistence: size: 10Gi
valkey: enabled: true
redpanda: enabled: true
cerbos: enabled: true
ingressRoute: enabled: true tls: enabled: true certResolver: letsencrypt
destination: server: https://kubernetes.default.svc namespace: shoehorn
# --- Sync Policy --- syncPolicy: automated: prune: true # Clean up resources removed from the chart selfHeal: true # Revert manual drift back to Git state allowEmpty: false # Safety net -- never delete ALL resources syncOptions: - CreateNamespace=true - ServerSideApply=true # Avoids 262KB annotation size limit - PrunePropagationPolicy=foreground - PruneLast=true # Apply new resources before pruning old ones - RespectIgnoreDifferences=true retry: limit: 3 backoff: duration: 30s factor: 2 maxDuration: 5m
# --- Ignore Differences --- # Fields managed by other controllers that ArgoCD should not fight over ignoreDifferences: # cert-manager injects caBundle after creation - group: admissionregistration.k8s.io kind: MutatingWebhookConfiguration jsonPointers: - /webhooks/0/clientConfig/caBundle - group: admissionregistration.k8s.io kind: ValidatingWebhookConfiguration jsonPointers: - /webhooks/0/clientConfig/caBundle # HPA manages replica count -- don't fight it - group: apps kind: Deployment jsonPointers: - /spec/replicasStep 4: Apply
Section titled “Step 4: Apply”kubectl apply -f argocd/project.yamlkubectl apply -f argocd/application.yamlOr with the CLI:
argocd app create shoehorn -f argocd/application.yamlargocd app sync shoehornManaging Secrets
Section titled “Managing Secrets”Never store secrets in the Application values. Use External Secrets Operator:
apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: database-credentials namespace: shoehornspec: refreshInterval: 1h secretStoreRef: name: aws-secretsmanager # or vault, gcp, azure kind: ClusterSecretStore target: name: database-credentials data: - secretKey: postgres_password remoteRef: key: shoehorn/database property: postgres_password - secretKey: db_password remoteRef: key: shoehorn/database property: db_passwordDeploy the ESO controller independently from ArgoCD (or in a separate “infra” Application with Prune=false). Never let the secret management system be managed by the same Application that depends on it.
Upgrading Shoehorn
Section titled “Upgrading Shoehorn”- Update
targetRevisioninapplication.yamlto the new chart version - Review the changelog for breaking changes
- Commit and push — ArgoCD auto-syncs the upgrade
- Monitor:
argocd app get shoehornargocd app wait shoehorn --healthAutomate version bumps with Renovate or Dependabot creating PRs against your GitOps repo.
Multi-Source Values (Recommended for Teams)
Section titled “Multi-Source Values (Recommended for Teams)”For larger configurations, separate the chart source from the values file using multi-source Applications (ArgoCD 2.6+):
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: shoehorn namespace: argocdspec: project: shoehorn sources: # Source 1: The Helm chart from OCI - repoURL: ghcr.io/shoehorn-dev/helm-charts chart: shoehorn targetRevision: 0.1.0 helm: releaseName: shoehorn valueFiles: - $values/shoehorn/values-production.yaml # Source 2: Values from your Git repo - repoURL: https://github.com/acme-corp/gitops-config targetRevision: main ref: values destination: server: https://kubernetes.default.svc namespace: shoehornThis keeps values version-controlled in your own repo with full PR review workflow.
Multi-Cluster Deployment
Section titled “Multi-Cluster Deployment”Use an ApplicationSet with preserveResourcesOnDeletion to prevent accidental deletion:
apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata: name: shoehorn namespace: argocdspec: generators: - list: elements: - cluster: staging url: https://staging-cluster.example.com revision: 0.2.0-rc.1 domain: shoehorn-staging.example.com - cluster: production url: https://kubernetes.default.svc revision: 0.1.0 domain: shoehorn.example.com template: metadata: name: "shoehorn-{{cluster}}" spec: project: shoehorn source: repoURL: ghcr.io/shoehorn-dev/helm-charts chart: shoehorn targetRevision: "{{revision}}" helm: values: | global: domain: "{{domain}}" environment: "{{cluster}}" destination: server: "{{url}}" namespace: shoehorn syncPolicy: automated: prune: true selfHeal: true allowEmpty: false # Prevent cascade-deletion if an ApplicationSet element is removed syncPolicy: preserveResourcesOnDeletion: trueResource Tracking
Section titled “Resource Tracking”If you see app.kubernetes.io/instance label conflicts (ArgoCD vs Helm both setting it), switch to annotation-based tracking in argocd-cm:
data: application.resourceTrackingMethod: annotation+labelUse annotation+label as a migration step before moving to annotation only. Sync all Applications before changing the method to avoid orphaning resources.
Monitoring
Section titled “Monitoring”# Application health and sync statusargocd app get shoehorn
# Watch a sync in progressargocd app sync shoehorn --watch
# Sync historyargocd app history shoehorn
# Diff what would changeargocd app diff shoehorn