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.9" # 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:
migrations:
commands: # once per deploy, before any pod rolls
- ["./node_modules/.bin/prisma", "migrate", "deploy"]
cron:
- name: nightly-sync # ← same image, different command
schedule: "0 3 * * *" # Europe/Amsterdam time
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:
migrations:
commands:
- ["php", "craft", "up"]
- ["php", "craft", "cacheable/flush/config"]
workers:
- name: queue
replicas: 1
command: ["php", "craft", "queue/listen", "--verbose=1", "--color=0"]

php-generic — Craft CMS with Varnish​

Sites that use setCacheControl() / includeEsiBlock() from oym/craft-cacheable need component-varnish in front of the app. On top of the Craft block above:

component-varnish:
enabled: true
vcl:
existingConfigMap: varnish-vcl # the VCL file in overlays/<env>/, see the component docs
component-traefik-ingress:
hosts:
- host: <slug>.staging.k8s.instance.onyourmarks.tech
paths:
- path: /
pathType: Prefix
service:
name: varnish-service # Traefik -> Varnish -> app
port: 80
component-redis:
enabled: true
maxmemory: 1gb # the Craft cache lives here; size from the current server
resources:
requests: { cpu: 100m, memory: 512Mi }
limits: { cpu: 500m, memory: 1280Mi }
component-tasks:
migrations:
commands:
- ["php", "craft", "up"]
- ["php", "craft", "cacheable/flush/config"]
workers:
- name: queue
replicas: 1
command: ["php", "craft", "queue/listen", "--verbose=1", "--color=0"]
- name: cacheable-queue
replicas: 1
command: ["php", "craft", "cacheable-queue/listen", "--verbose=1", "--color=0"]
- name: cacheable-tag-queue
replicas: 1
command: ["php", "craft", "cacheable-tag-queue/listen", "--verbose=1", "--color=0"]

Add overlaysDir: k8s/app/platform/<slug>/overlays to xplatformapp.yaml and create overlays/<env>/{kustomization.yaml,default.vcl} for every environment. VARNISH_SERVER and VARNISH_DEFAULT_MAX_AGE go into Infisical (step 2). circuit-zandvoort is the reference project.

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. ArgoCD picks the definition up on its own within a few minutes. Then go to step 6 to watch it come up.

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