Skip to main content

Step 5 — Describe the app in oym-k8s-cluster

Everything so far happened in the project. Now you tell the cluster about it. That is three small files in this repo, in one push to main:

k8s/app/platform/<slug>/
├── definition/
│ └── xplatformapp.yaml ← "this app exists, uses chart X, has these environments"
└── helm/
├── values-staging.yaml ← how to run it on staging
└── values-production.yaml ← how to run it on production

Nothing else. No ArgoCD Application, no Kargo config, no namespace: a Crossplane composition generates all of that from xplatformapp.yaml (ArgoCD Application <slug>-<env>, namespace <slug>-<env>, Kargo warehouse and stage per environment). If you're curious it's in k8s/app/system/crossplane-system/config/composition/composition-xplatformapps.yaml.

5.1 definition/xplatformapp.yaml

apiVersion: platform.onyourmarks.tech/v1alpha1
kind: XPlatformApp
metadata:
name: <slug>
spec:
parameters:
app: <slug>
chart: sveltekit # sveltekit | node-generic | php-generic | symfony
chartVersion: "1.3.3" # current published version of that chart, see below
environments:
- staging
- production

Get the chart version from this repo (don't guess; ArgoCD can only pull versions that were published):

grep '^version:' k8s/helm/charts/<chart>/Chart.yaml

Rules:

  • metadata.name and parameters.app are both the slug.
  • environments decides how many values files you need: exactly one values-<env>.yaml per entry. A production-only app lists only production and has only values-production.yaml (see nn-backstory).
  • Allowed environment names: staging, production, sprint. Nothing else (the ArgoCD project rejects other namespaces).
  • Optional overlaysDir: k8s/app/platform/<slug>/overlays adds extra Kubernetes manifests per environment (a MongoDB or Qdrant StatefulSet, for example). See oym-chant-fe for the pattern; you need overlays/<env>/kustomization.yaml for every environment listed.

5.2 The values files

Start from the closest project in k8s/app/platform/ (same chart, same needs). Below are complete, annotated files. Lines with # ← are the ones you change.

Naming trap, read this first

prod vs production

Production uses two different words and both are required:

WhereWord
global.environmentSlug in values-production.yamlprod — this is the Infisical environment and the DB name prefix (prod-<slug>)
Everything else: file name values-production.yaml, environments: entry, image tag suffix -production, hostname .prod., namespace <slug>-productionproduction

Staging is simply staging everywhere.

sveltekit — staging

global:
projectSlug: <slug> # ←
environmentSlug: staging
image:
repository: registry.gitlab.com/onyourmarks/k8s/<slug>/app # ←
tag: initial # Kargo replaces this after the first tag:image:staging
component-traefik-ingress:
enabled: true
labels:
monitoring.onyourmarks.tech/probe: "true" # uptime monitoring; use "allow-404" for APIs with no page on /
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-redirect-https@kubernetescrd,traefik-block-robots@kubernetescrd
hosts:
- host: <slug>.staging.k8s.instance.onyourmarks.tech # ←
paths:
- path: /
pathType: Prefix
tls:
- secretName: traefik-ingress-tls
hosts:
- <slug>.staging.k8s.instance.onyourmarks.tech # ←

traefik-block-robots keeps staging out of search engines. Keep it on every *.k8s.instance.onyourmarks.tech host, never on a public domain.

sveltekit — production

global:
projectSlug: <slug> # ←
environmentSlug: prod # yes, "prod"
image:
repository: registry.gitlab.com/onyourmarks/k8s/<slug>/app # ←
tag: initial # Kargo replaces this after the first tag:image:production
component-traefik-ingress:
enabled: true
labels:
monitoring.onyourmarks.tech/probe: "true"
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-redirect-https@kubernetescrd
hosts:
- host: <slug>.prod.k8s.instance.onyourmarks.tech # ← always keep this one
paths:
- path: /
pathType: Prefix
- host: www.example.com # ← the real domain, once DNS points here
paths:
- path: /
pathType: Prefix
tls:
- secretName: traefik-ingress-tls
hosts:
- <slug>.prod.k8s.instance.onyourmarks.tech # ←
- www.example.com # ←

Custom domains:

  • Point the domain's A record at 57.129.55.146 (the cluster's Traefik load balancer). Let's Encrypt only issues the certificate once the name resolves to the cluster, so until then that host serves a default cert.
  • Apex + www: list both hosts and pick a redirect middleware: traefik-redirect-https@kubernetescrd,traefik-redirect-to-www@kubernetescrd (or traefik-redirect-to-non-www@kubernetescrd).
  • Don't add the custom domain until you are ready to switch traffic; the .prod.k8s… host is enough to test production.

node-generic — with database and cron jobs

Same as sveltekit plus the components you need. Staging shown; production differs only in the global block, hosts and replicaCount.

global:
projectSlug: <slug> # ←
environmentSlug: staging
image:
repository: registry.gitlab.com/onyourmarks/k8s/<slug>/app # ←
tag: initial
component-traefik-ingress:
enabled: true
labels:
monitoring.onyourmarks.tech/probe: "allow-404" # an API: / may 404
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-redirect-https@kubernetescrd,traefik-block-robots@kubernetescrd
hosts:
- host: <slug>.staging.k8s.instance.onyourmarks.tech # ←
paths:
- path: /
pathType: Prefix
tls:
- secretName: traefik-ingress-tls
hosts:
- <slug>.staging.k8s.instance.onyourmarks.tech # ←
component-database:
enabled: true # creates DB + user stg-<slug>; password read from Infisical
parameters:
passwordField: DATABASE_PASSWORD # ← the Infisical key you chose in step 2
# user: shorter-name # ← only if "prod-<slug>" exceeds 32 chars; must match the Infisical values
# additionalDatabases: # ← e.g. a shadow DB for Prisma migrations
# - <slug>-migration
component-tasks:
cron:
- name: nightly-sync # ← same image, different command
schedule: "0 3 * * *"
command: ["node", "dist/src/cli.js", "sync"]
# workers: # long-running processes (queues)
# - name: queue
# replicas: 1
# command: ["node", "dist/src/worker.js"]
# manual: # run on demand, see component-tasks docs
# - name: import
# command: ["node", "dist/src/cli.js", "import"]

php-generic — Craft CMS

global:
projectSlug: <slug> # ←
environmentSlug: staging
image:
repository: registry.gitlab.com/onyourmarks/k8s/<slug>/app # ←
tag: initial
sharedVolumes: # Craft writes to public/cache; all pods must see the same files
- name: shared
size: 5Gi
mounts:
- path: /app/public/cache
subPath: public-cache
component-traefik-ingress:
enabled: true
labels:
monitoring.onyourmarks.tech/probe: "true"
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-redirect-https@kubernetescrd,traefik-block-robots@kubernetescrd
hosts:
- host: <slug>.staging.k8s.instance.onyourmarks.tech # ←
paths:
- path: /
pathType: Prefix
tls:
- secretName: traefik-ingress-tls
hosts:
- <slug>.staging.k8s.instance.onyourmarks.tech # ←
component-database:
enabled: true
parameters:
passwordField: CRAFT_DB_PASSWORD
component-redis:
enabled: true # cache, sessions, queue; reachable as redis-service:6379
component-tasks:
workers:
- name: queue
replicas: 1
command: ["php", "craft", "queue/listen", "--verbose=1", "--color=0"]

php-generic — api-simple without health endpoints

Add this block if you skipped the endpoints in step 1 (only php-generic and node-generic allow it):

probes:
startup:
enabled: false
readiness:
enabled: false
liveness:
enabled: false
component-database:
enabled: true
parameters:
passwordField: DB_PASSWORD

Other knobs you may need

NeedValues
More or fewer podsreplicaCount: 1 (default 2; staging often runs 1 to save resources)
More memoryresources: { requests: { cpu: 250m, memory: 384Mi }, limits: { memory: 768Mi } } (default limit 512Mi)
Readiness on a different pathprobes: { readiness: { path: /health } }
Files shared between pods (uploads)global.sharedVolumes, see shared storage
Security headersappend traefik-security-headers@kubernetescrd to the middlewares

Full reference: components.

5.3 Check it renders

From the repo root, before pushing:

helm dependency update k8s/helm/charts/<chart>
helm template <slug> k8s/helm/charts/<chart> -f k8s/app/platform/<slug>/helm/values-staging.yaml > /dev/null
helm template <slug> k8s/helm/charts/<chart> -f k8s/app/platform/<slug>/helm/values-production.yaml > /dev/null

No output means the YAML is valid and every key is where the chart expects it. A typo in a key name doesn't error in Helm, it just silently does nothing, so also eyeball git diff against the project you copied from.

5.4 Push

Commit the three files on main as feat: add <slug> platform app and push. Then go to step 6. Pushing alone does not deploy anything yet.

Let Claude Code do it

The cluster prompt from the prompt generator creates the three files from the same choices as the project prompt and checks that they render. You review the diff and push. Run it inside your checkout of oym-k8s-cluster.

Done when

  • definition/xplatformapp.yaml with the current chartVersion
  • One helm/values-<env>.yaml per environment; environmentSlug is prod for production
  • passwordField equals the Infisical key name from step 2
  • helm template passes for every values file
  • Pushed to main