GitLab CI to Kubernetes is the default delivery path on DarDevOps engagements: one pipeline builds your app, publishes a immutable container image, and promotes it to clusters with explicit gates between merge request, staging, and production. This article walks through a practical pipeline you can adapt without adopting every enterprise feature on day one.
We assume GitLab as forge, a container registry (GitLab Registry or Harbor), and a Kubernetes cluster you control self-hosted on OVH-style VPS or managed elsewhere. The goal is repeatable deploys with audit trail in git, not click-ops on Friday evenings.
Pipeline stages that actually matter
- lint + unit tests on every push
- build Docker image with deterministic Dockerfile and BuildKit cache
- scan image for critical CVEs (fail or warn based on policy)
- push image tagged with commit SHA and semver on tags
- deploy to review namespace on merge requests
- deploy to staging on main merge
- manual or canary promote to production
Skipping tests on main is how teams lose velocity not gain it. Keep fast jobs under five minutes; split slow integration tests into nightly unless they block revenue paths.
Example.gitlab-ci.yml skeleton
stages: [test, build, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
unit:
stage: test
script:
- npm ci && npm test
build:
stage: build
script:
- docker build -t $IMAGE.
- docker push $IMAGE
deploy_staging:
stage: deploy
environment: staging
script:
- kubectl set image deploy/api api=$IMAGE -n staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy_prod:
stage: deploy
environment: production
when: manual
script:
- kubectl set image deploy/api api=$IMAGE -n productionKubernetes access from GitLab runners
Runners need kubeconfig or GitLab's cluster integration with scoped service accounts. Prefer namespace-limited tokens per environment staging CI must not delete production. Store KUBECONFIG or GitLab masked variables; rotate tokens when people leave. For self-hosted runners on the same VPS as k3s, network policies still matter: compromise of a runner should not mean cluster-admin.
Secrets, config, and migrations
Never bake secrets into images. Use Kubernetes Secrets sourced from Vault, SOPS-encrypted manifests in git, or GitLab protected variables injected at deploy time. ConfigMaps hold non-secret toggles. Database migrations run as Helm pre-upgrade hooks or one-off Jobs with the same image tag you are releasing not from a developer laptop.

Review apps and staging fidelity
Merge request environments catch integration bugs before main. Spin a namespace per MR, deploy the MR image, post URL to GitLab. Tear down on merge. Staging must mirror production topology same ingress controller, same Postgres major version, same resource limits class or you are testing fiction.
Rollback and observability hooks
Keep previous image tags addressable. kubectl rollout undo is your ten-minute lifesaver we document it in our rollback runbook. Emit deploy annotations to Prometheus or Loki so on-call can correlate error spikes with pipeline ID. Portainer helps operators who prefer UI over kubectl for quick inspections; we compare both in Portainer vs kubectl.
DarDevOps applies these patterns to internal workloads DarDevLab, mailer stack, CRM adjacent services before recommending them on client clusters. That is Deployed Realities for infrastructure: pipelines we run ourselves.
Caching, tags, and registry hygiene
Use BuildKit cache mounts in GitLab Docker executors to speed npm and apt layers. Tag every release image with $CI_COMMIT_SHA; add semver tags only on git tags. Retention policies in GitLab Registry prevent disk fill on small VPS hosts. Scan images in CI block critical CVEs on production paths, warn on staging.
Multi-service repos and monorepos
Use rules:changes paths so a documentation edit does not rebuild every microservice. child pipelines or parallel matrix jobs keep monorepo CI under acceptable latency. Shared libraries publish internal packages on tag; services consume pinned versions to avoid surprise breaks on main.
When you outgrow kubectl set image, adopt Helm charts per service with values files for staging and production. GitOps repos then become the contract between CI (build artifact) and cluster (desired state) CI finishes at push; Flux reconciles deploy.
Start simple: one service, one cluster, one manual production gate. Measure lead time and failed deploy rate for a month. Then add GitOps, canaries, or multi-region each layer justified by incident data, not conference FOMO. DarDevOps engagements begin with this baseline before discussing enterprise add-ons.
GitLab shared runners or self-hosted?
Self-hosted runners on your network reduce image pull latency and keep deploy traffic off shared quotas. Shared runners are fine for open-source or early MVP if secrets are scoped tightly.
Helm or plain manifests?
Plain kubectl works for one service. Helm or Kustomize pays off at three-plus services or multiple environments. GitOps adds reviewable cluster state worth it when ops team grows.
How do I gate production?
Use when: manual on prod job, protected branches, and optional canary Service or Flagger. Never auto-deploy prod on every green main build without stakeholder sign-off.
Where does DarDev run this?
On Kubernetes clusters we operate for DarDev products and selected client engagements GitLab, Portainer, Prometheus, and Grafana in the reference set on dardev.net/products.



