Publishing a project to the cluster
This is the end-to-end, no-prior-knowledge guide for taking a project from "runs on my laptop" to "runs on
https://<project>.staging.k8s.instance.onyourmarks.tech" and, later, on its production domain.
It is written in the order the work actually happens. The cluster repo is the last step, not the first. Most of the work is in the project itself, and most of the mistakes happen there too.
Every step has a reference project you copy from. The prompt generator turns your stack, slug and a few choices into two Claude Code prompts that do steps 1 to 5. Read the "Traps" of each page, run the prompts, check the "Done when" lists. That's it.
How a deploy works (30-second version)
your repo GitLab CI GitLab registry
┌──────────────┐ push ┌──────────────────┐ push ┌──────────────────────────────────┐
│ .deploy/ │ ───────► │ build:image │ ─ ─────► │ …/k8s/<slug>/app:<sha> │
│ .gitlab-ci │ │ tag:image:staging│ ──────► │ …/k8s/<slug>/app:<sha>-staging │
└──────────────┘ └──────────────────┘ └──────────────────────────────────┘
│ webhook "warehouse: <slug>-staging"
▼
┌──────────────────┐ commit ┌──────────────────────────────────┐
│ Kargo │ ───────► │ oym-k8s-cluster (main) │
│ (watches tags) │ │ k8s/app/platform/<slug>/helm/ │
└──────────────────┘ │ values-staging.yaml tag: … │
└───────────── ─────────────────────┘
│ sync
▼
┌──────────────────────────────────┐
│ ArgoCD → namespace <slug>-staging │
│ pods, ingress, DB user, secrets │
└──────────────────────────────────┘
- CI builds one image per commit and pushes it as
<sha>. - You click a manual job (
tag:image:stagingortag:image:production). That re-tags the image as<sha>-staging/<sha>-productionand pings Kargo. There is no "deploy on push to main". - Kargo sees the new tag and commits it into this repo's values file for that environment.
- ArgoCD sees the commit and rolls the new image out.
Secrets never travel through CI. Pods read them from a Kubernetes Secret that is synced from Infisical
(project = your slug, environment = staging or prod).
Before you start
| You need | Where to get it |
|---|---|
Membership of the GitLab group onyourmarks/k8s | Ask DevOps. The project must live at gitlab.com/onyourmarks/k8s/<slug>. |
| Infisical account (eu.infisical.com) with rights to create projects | Ask DevOps. |
Infisical CLI on your laptop (brew install infisical/get-cli/infisical) | Needed for .env locally. |
| Docker Desktop | To build-test your image before CI does. |
| Claude Code | Runs the prompts from the prompt generator. |
| Cluster kubeconfig + Auth0 login | Only for the very last step. Set it up via Connecting to MySQL (same kubeconfig). |
A local checkout of oym-k8s-cluster (this repo) | For the cluster step. |
Pick your slug
The slug is the one name that ties everything together. It is used, unchanged, as:
- the GitLab project path (
onyourmarks/k8s/<slug>) and thereforeCI_PROJECT_NAME - the container image path (
registry.gitlab.com/onyourmarks/k8s/<slug>/app) - the Infisical project slug
- the Kubernetes namespaces (
<slug>-staging,<slug>-production) - the Kargo warehouse names (
<slug>-staging,<slug>-production) - the MySQL user and database (
stg-<slug>,prod-<slug>) - the directory in this repo (
k8s/app/platform/<slug>/)
Rules: lowercase, digits and hyphens only, and keep it short (MySQL user names cap at 32 characters including
the prod- prefix). Examples: ziggo-espn-quiz-fe, nn-backstory, unisport-agent.
Every system above derives its names from the slug. Decide it before step 2 and never change it.
Is my project already on the cluster?
Open the project's .gitlab-ci.yml:
| You see | Meaning |
|---|---|
stage-tag-environment and a file .deploy/docker/build/app.Dockerfile | Already a k8s project. Skip to whichever step is broken. |
stage-deploy, SSH_STAGING, SERVER_FOLDER_*, or oym-gitlab-deploy-templates@4.x | Legacy VM deploy (Deployer over SSH). Follow this guide from step 1; the old CI file gets replaced. |
Raw https://raw.githubusercontent.com/... includes | Very old legacy. Same as above. |
| Nothing | New project. Follow this guide from step 1. |
Pick your app chart
The cluster has four Helm "app charts". Your project uses exactly one; the choice decides the port your container must listen on and which reference project you copy from.
| Your stack | Chart | Container port | Reference project |
|---|---|---|---|
| SvelteKit (adapter-node) | sveltekit | 3000 | ziggo-espn-quiz-fe |
| NestJS / Express / any Node server | node-generic | 3000 | nn-backstory (NestJS + Prisma), hardloopsupporter-api (Express, no build step) |
| Craft CMS | php-generic | 80 | onyourmarks-website |
| api-simple / other PHP | php-generic | 80 | ziggo-espn-quiz-be |
| Symfony | symfony | 80 | wedstrijd-van-je-leven |
All reference projects live at gitlab.com/onyourmarks/k8s/<name>. Chart details: App charts.
oym-paddock-fe and team-yellowb-tourshirt-2026 carry an old, vendored copy of the CI templates in
.deploy/templates/. Their .gitlab-ci.yml will not notify Kargo. Their Dockerfiles are fine; their CI files are not.
The steps
| # | Step | Where | Page |
|---|---|---|---|
| 1 | Make the app container-ready: health endpoints, port, runtime config | project repo | Project requirements |
| 2 | Infisical project: slug, environments, machine identity, secrets | Infisical + project repo | Infisical |
| 3 | .deploy/ folder: Dockerfile and friends | project repo | The .deploy folder |
| 4 | .gitlab-ci.yml and the first image | project repo + GitLab | GitLab CI |
| 5 | Values files + definition in this repo | oym-k8s-cluster | Cluster repo |
| 6 | Apply, watch it come up, go to production | terminal + UIs | Go live |
Do them in order. Step 5 depends on names chosen in step 2, and step 6 depends on an image produced in step 4.