TL;DR

All GitLab CI config lives in .gitlab-ci.yml at the repo root. Use stages to sequence jobs, rules: to control when jobs run, environments: for deployment tracking, and protected variables for secrets. Register a self-managed runner on your node for builds that need cluster network access.

Basic Pipeline Structure

A minimal pipeline with test, build, and deploy stages — covers the most common CI flow. Jobs in the same stage run in parallel; stages run sequentially.

yaml.gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

default:
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

test:
  stage: test
  image: python:3.12
  script:
    - pip install -r requirements.txt
    - pytest tests/ -v --tb=short
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

build:
  stage: build
  script:
    - docker build -t $IMAGE .
    - docker push $IMAGE
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

deploy-staging:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.myapp.example.com
  script:
    - kubectl set image deployment/myapp myapp=$IMAGE -n staging
    - kubectl rollout status deployment/myapp -n staging --timeout=5m
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

Docker Build with GitLab Registry

Use $CI_REGISTRY_IMAGE to push images to the project's built-in GitLab container registry. Tag with both the commit SHA (for traceability) and a mutable tag like latest.

yaml.gitlab-ci.yml
build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    CACHE_IMAGE: "$CI_REGISTRY_IMAGE:cache"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    # Pull cache layer to speed up builds
    - docker pull $CACHE_IMAGE || true
    - docker build
        --cache-from $CACHE_IMAGE
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
        --tag $CI_REGISTRY_IMAGE:latest
        --build-arg BUILDKIT_INLINE_CACHE=1
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
    - docker push $CACHE_IMAGE
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Kubernetes Deploy Job

Store the kubeconfig as a GitLab CI/CD file variable (base64-encoded). The deploy job decodes it and runs kubectl or helm. Use environment: to track deployments in GitLab's Environments UI.

yaml.gitlab-ci.yml
deploy-production:
  stage: deploy
  image: dtzar/helm-kubectl:3.14
  environment:
    name: production
    url: https://myapp.example.com
  variables:
    NAMESPACE: production
  before_script:
    # KUBECONFIG is a CI/CD file variable containing the base64-encoded kubeconfig
    - echo "$KUBECONFIG_B64" | base64 -d > /tmp/kube.conf
    - export KUBECONFIG=/tmp/kube.conf
    - kubectl cluster-info
  script:
    - helm upgrade --install myapp ./charts/myapp
        --namespace $NAMESPACE
        --set image.repository=$CI_REGISTRY_IMAGE
        --set image.tag=$CI_COMMIT_SHORT_SHA
        --wait --timeout 5m
  after_script:
    - rm -f /tmp/kube.conf
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: manual  # require manual approval for production
  environment:
    name: production
    action: start

GitLab Runner Setup

For builds that need access to cluster internals or private networks, register a self-managed runner on a node inside the cluster. The kubernetes executor runs each job as a Pod, avoiding static infrastructure.

bashrunner-setup.sh
# Install GitLab Runner on Kubernetes using Helm
helm repo add gitlab https://charts.gitlab.io
helm repo update

helm upgrade --install gitlab-runner gitlab/gitlab-runner \
  --namespace gitlab \
  --create-namespace \
  --set gitlabUrl=https://gitlab.example.com \
  --set runnerToken="$GITLAB_RUNNER_TOKEN" \
  --set rbac.create=true \
  --set executor=kubernetes

# Check runner is connected
kubectl get pods -n gitlab
kubectl logs -n gitlab -l app=gitlab-runner -f

# Register a shell runner on a VM (alternative)
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --registration-token "$REGISTRATION_TOKEN" \
  --executor "shell" \
  --description "my-shell-runner" \
  --tag-list "shell,deploy"

Useful Predefined Variables

VariableValue
$CI_COMMIT_SHORT_SHA8-char commit SHA
$CI_COMMIT_TAGGit tag (empty if no tag)
$CI_COMMIT_BRANCHBranch name
$CI_DEFAULT_BRANCHProject default branch (e.g. main)
$CI_REGISTRY_IMAGEGitLab registry path for this project
$CI_REGISTRY_USERAuto-generated registry username
$CI_REGISTRY_PASSWORDAuto-generated registry password
$CI_PIPELINE_SOURCEpush / merge_request_event / schedule / …
$CI_ENVIRONMENT_NAMEName of the current environment