Skip to main content

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.

You don't need to write a Dockerfile from scratch

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 │
└──────────────────────────────────┘
  1. CI builds one image per commit and pushes it as <sha>.
  2. You click a manual job (tag:image:staging or tag:image:production). That re-tags the image as <sha>-staging / <sha>-production and pings Kargo. There is no "deploy on push to main".
  3. Kargo sees the new tag and commits it into this repo's values file for that environment.
  4. 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 needWhere to get it
Membership of the GitLab group onyourmarks/k8sAsk DevOps. The project must live at gitlab.com/onyourmarks/k8s/<slug>.
Infisical account (eu.infisical.com) with rights to create projectsAsk DevOps.
Infisical CLI on your laptop (brew install infisical/get-cli/infisical)Needed for .env locally.
Docker DesktopTo build-test your image before CI does.
Claude CodeRuns the prompts from the prompt generator.
Cluster kubeconfig + Auth0 loginOnly 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 therefore CI_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.

Renaming later is painful

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 seeMeaning
stage-tag-environment and a file .deploy/docker/build/app.DockerfileAlready a k8s project. Skip to whichever step is broken.
stage-deploy, SSH_STAGING, SERVER_FOLDER_*, or oym-gitlab-deploy-templates@4.xLegacy VM deploy (Deployer over SSH). Follow this guide from step 1; the old CI file gets replaced.
Raw https://raw.githubusercontent.com/... includesVery old legacy. Same as above.
NothingNew 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 stackChartContainer portReference project
SvelteKit (adapter-node)sveltekit3000ziggo-espn-quiz-fe
NestJS / Express / any Node servernode-generic3000nn-backstory (NestJS + Prisma), hardloopsupporter-api (Express, no build step)
Craft CMSphp-generic80onyourmarks-website
api-simple / other PHPphp-generic80ziggo-espn-quiz-be
Symfonysymfony80wedstrijd-van-je-leven

All reference projects live at gitlab.com/onyourmarks/k8s/<name>. Chart details: App charts.

Two projects you must NOT copy CI from

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

#StepWherePage
1Make the app container-ready: health endpoints, port, runtime configproject repoProject requirements
2Infisical project: slug, environments, machine identity, secretsInfisical + project repoInfisical
3.deploy/ folder: Dockerfile and friendsproject repoThe .deploy folder
4.gitlab-ci.yml and the first imageproject repo + GitLabGitLab CI
5Values files + definition in this repooym-k8s-clusterCluster repo
6Apply, watch it come up, go to productionterminal + UIsGo 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.