Skip to content

Add a new application

Adding an app means committing a directory to the repo. Argo CD notices it and syncs it for you, so there is no kubectl apply and no manual step against the cluster. This guide walks through what that directory looks like and the conventions worth knowing.

Every app lives under kubernetes/cluster/active/apps/<name>/ and is split into a base and a prod overlay. The overlay is the unit Argo CD syncs, and a small config.json inside it is what gets the app noticed: an ApplicationSet watches the repo for apps/**/overlays/*/config.json and turns each one it finds into an Argo CD Application that it keeps in sync. So the whole job is writing files and pushing them.

The layout:

📁 apps/cyberchef/
📁 base/
📄 kustomization.yaml
📄 values.yaml
📁 overlays/prod/
📄 kustomization.yaml
📄 values-override.yaml
📄 config.json
📄 replacements.yaml
📁 resources/
📁 external-secrets/

replacements.yaml, resources/, and external-secrets/ are optional. Add them only when the app needs them, for example an HTTPRoute under resources/.

Most apps wrap a Helm chart with Kustomize, which keeps the chart’s values in the repo and lets the overlay layer changes on top. It is the common pattern here, not a requirement: if no chart fits, a plain Kustomization of hand-written manifests works too.

When you do use a chart, the rough order of preference is the project’s official chart, then TrueCharts, then the bjw-s app-template (for apps which lack a good Helm chart). CyberChef uses the TrueCharts chart. Inspect a chart before committing to it:

Terminal window
helm show chart oci://oci.trueforge.org/truecharts/cyberchef
helm show values oci://oci.trueforge.org/truecharts/cyberchef

base/values.yaml holds the stable, non-secret configuration: image, ports, persistence intent, anything intrinsic to the app. Keep it small, or leave it empty when the chart defaults are fine, as CyberChef does. The base kustomization usually just exists to be extended:

base/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []

The overlay pulls the chart with helmCharts and layers values-override.yaml (environment-specific values) on top of the base. Here is CyberChef’s overlay, which uses a TrueCharts chart and exposes itself publicly:

overlays/prod/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- resources/httproute.yaml # public apps only, see step 5
helmCharts:
- name: cyberchef
repo: oci://oci.trueforge.org/truecharts
version: 13.5.0 # pinned, kept current by Renovate
releaseName: cyberchef
namespace: cyberchef
valuesFile: ../../base/values.yaml
additionalValuesFiles:
- values-override.yaml
includeCRDs: true

name is the chart’s name and has to match what the registry publishes. releaseName and namespace are your choices. They line up here because the chart happens to be called cyberchef, but with a generic chart like app-template the name would be app-template while the release and namespace stay your app’s name.

This is the file the ApplicationSet discovers. It is mostly the app name and the namespace it deploys into:

overlays/prod/config.json
{
"appName": "cyberchef",
"destName": "in-cluster",
"destNamespace": "cyberchef",
"destServer": "https://kubernetes.default.svc",
"project": "default",
"userGivenName": "cyberchef",
"annotations": {},
"labels": {}
}

Pick the path that matches how the app should be reached (see traffic flow):

  • Public: add an HTTPRoute under resources/, with a parentRef to the http-gateway-api gateway in envoy-gateway-ingress, on a hostname under ${FQDN_01}. This is what CyberChef does:
overlays/prod/resources/httproute.yaml
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cyberchef
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: http-gateway-api
namespace: envoy-gateway-ingress
hostnames:
- "cyberchef.${FQDN_01}"
rules:
- backendRefs:
- kind: Service
name: cyberchef
namespace: cyberchef
port: 10219
matches:
- path:
type: PathPrefix
value: /
  • Tailnet only: use a Tailscale Ingress, either through the chart’s Ingress values or an Ingress with ingressClassName: tailscale and the tailscale.com/proxy-group: ingress-proxies annotation, on a hostname under ${FQDN_02}.

The route lives in its own file rather than in the chart’s values because, unlike Ingress, the Gateway API’s HTTPRoute is not yet supported by many charts. When a chart does expose route values, you can define it there instead of adding a separate resource.

Real domains never go in the repo. Hostnames stay as ${FQDN_01} or ${FQDN_02}, and replacements.yaml (with a configMapGenerator that reads /tmp/env-vars.env) substitutes the real value when the manifests are rendered. To list the app on the Homepage dashboard, add the gethomepage.dev/* annotations to its route.

Apps that need secrets or a database follow established patterns: External Secrets manifests under external-secrets/, and a CloudNativePG cluster under resources/ for PostgreSQL. The outline app is a good reference for both.

Build the overlay the same way Argo CD does, and read the output rather than just checking it passes. Confirm the hostname substitution landed and that the route points at the rendered Service name and port:

Terminal window
kubectl kustomize --enable-helm --load-restrictor=LoadRestrictionsNone \
kubernetes/cluster/active/apps/cyberchef/overlays/prod

Argo CD owns the committed state, so the normal path never includes kubectl apply.

Push to the repo. The ApplicationSet picks up the new config.json, creates the Application, and syncs it, creating the namespace and self-healing on drift. Check it landed:

Terminal window
kubectl get applications -n argocd | grep cyberchef
kubectl get pods -n cyberchef