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.nameandparameters.appare both the slug.environmentsdecides how many values files you need: exactly onevalues-<env>.yamlper entry. A production-only app lists onlyproductionand has onlyvalues-production.yaml(seenn-backstory).- Allowed environment names:
staging,production,sprint. Nothing else (the ArgoCD project rejects other namespaces). - Optional
overlaysDir: k8s/app/platform/<slug>/overlaysadds extra Kubernetes manifests per environment (a MongoDB or Qdrant StatefulSet, for example). Seeoym-chant-fefor the pattern; you needoverlays/<env>/kustomization.yamlfor 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 productionProduction uses two different words and both are required:
| Where | Word |
|---|---|
global.environmentSlug in values-production.yaml | prod — 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>-production | production |
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(ortraefik-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
| Need | Values |
|---|---|
| More or fewer pods | replicaCount: 1 (default 2; staging often runs 1 to save resources) |
| More memory | resources: { requests: { cpu: 250m, memory: 384Mi }, limits: { memory: 768Mi } } (default limit 512Mi) |
| Readiness on a different path | probes: { readiness: { path: /health } } |
| Files shared between pods (uploads) | global.sharedVolumes, see shared storage |
| Security headers | append 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.yamlwith the currentchartVersion - One
helm/values-<env>.yamlper environment;environmentSlugisprodfor production -
passwordFieldequals the Infisical key name from step 2 -
helm templatepasses for every values file - Pushed to
main