How to Set Up GitLab CI/CD from Scratch (2026 Complete Tutorial)
A practical step-by-step guide to setting up GitLab CI/CD pipelines from zero — covering runners, pipeline stages, Docker builds, deployment to Kubernetes, and best practices.
GitLab CI/CD is one of the most complete CI/CD systems available — and it's built into GitLab, so there's no third-party integration to manage. Everything lives in one place: your code, your pipelines, your container registry, and your deployments.
This guide takes you from zero to a working pipeline that lints, tests, builds a Docker image, and deploys to Kubernetes.
What You'll Build
By the end of this guide, you'll have a GitLab CI/CD pipeline with these stages:
lint → test → build → push → deploy
- lint: Check code quality
- test: Run unit tests
- build: Build a Docker image
- push: Push to GitLab Container Registry
- deploy: Deploy to Kubernetes using
kubectl
Prerequisites
- A GitLab account (gitlab.com or self-hosted)
- A project with some code (we'll use a simple Node.js app)
- A Kubernetes cluster (optional — we'll show how to set this up)
- Docker installed on your machine for local testing
Step 1: Understand the Pipeline File
GitLab CI/CD is configured entirely in a single file: .gitlab-ci.yml at the root of your repository.
Every time you push code, GitLab reads this file and runs the pipeline defined in it.
The minimal structure:
# .gitlab-ci.yml
stages:
- lint
- test
- build
my-first-job:
stage: lint
script:
- echo "Running lint"
- npm run lintThree concepts to understand:
- stages: Define the order of execution. All jobs in a stage run in parallel. Stages run sequentially.
- jobs: The actual work. Each job has a
stageand ascript. - script: The shell commands to run.
Step 2: Set Up a GitLab Runner
A GitLab Runner is the machine that actually runs your pipeline jobs.
Option A: Use GitLab's Shared Runners (Easiest)
On gitlab.com, shared runners are available by default. Your pipelines will run on GitLab's managed infrastructure. Nothing to configure.
Go to Settings → CI/CD → Runners in your project to verify shared runners are enabled.
Option B: Register Your Own Runner (More Control)
If you want to use your own server or run Docker-in-Docker:
Install the runner on your server:
# On Ubuntu/Debian
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner
# Start the runner service
sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runnerRegister the runner with your GitLab project:
sudo gitlab-runner register \
--url "https://gitlab.com" \
--token "<your-project-runner-token>" \
--executor "docker" \
--docker-image "ubuntu:22.04" \
--description "my-project-runner"Get the registration token from: Settings → CI/CD → Runners → New project runner.
Step 3: Write Your First Real Pipeline
Here's a complete pipeline for a Node.js application:
# .gitlab-ci.yml
image: node:20-alpine # default Docker image for all jobs
stages:
- lint
- test
- build
- push
- deploy
variables:
REGISTRY: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
# ─── LINT ───────────────────────────────────────────────────
lint:
stage: lint
script:
- npm ci
- npm run lint
cache:
key:
files:
- package-lock.json
paths:
- .npm/
# ─── TEST ───────────────────────────────────────────────────
test:
stage: test
script:
- npm ci
- npm test
cache:
key:
files:
- package-lock.json
paths:
- .npm/
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# ─── BUILD ──────────────────────────────────────────────────
build-image:
stage: build
image: docker:24.0
services:
- docker:24.0-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $REGISTRY:$IMAGE_TAG -t $REGISTRY:latest .
- docker push $REGISTRY:$IMAGE_TAG
- docker push $REGISTRY:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# ─── DEPLOY ─────────────────────────────────────────────────
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- echo "$KUBECONFIG_DATA" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- kubectl set image deployment/my-app app=$REGISTRY:$IMAGE_TAG -n production
- kubectl rollout status deployment/my-app -n production
environment:
name: production
url: https://myapp.example.com
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Push this file and GitLab will run the pipeline automatically.
Step 4: Configure Variables and Secrets
GitLab CI uses variables for secrets. Never hardcode credentials in .gitlab-ci.yml.
Set variables in GitLab UI
Go to Settings → CI/CD → Variables and add:
| Variable | Value | Protected | Masked |
|---|---|---|---|
KUBECONFIG_DATA | base64 of your kubeconfig | ✅ | ✅ |
DOCKER_HUB_TOKEN | Docker Hub token | ✅ | ✅ |
Built-in GitLab variables (always available)
CI_REGISTRY = registry.gitlab.com
CI_REGISTRY_IMAGE = registry.gitlab.com/your-group/your-project
CI_REGISTRY_USER = gitlab-ci-token (for login)
CI_REGISTRY_PASSWORD = automatically set
CI_COMMIT_SHORT_SHA = abc123de (short git hash)
CI_COMMIT_BRANCH = main (branch name)
CI_PIPELINE_ID = 12345
CI_PROJECT_NAME = my-project
You don't need to set these — they're injected automatically.
Step 5: Use the GitLab Container Registry
GitLab has a built-in container registry — no Docker Hub needed.
Your image URL is always: registry.gitlab.com/your-group/your-project:tag
The login in before_script uses built-in variables:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRYPull from the registry in Kubernetes:
# Create an imagePullSecret from GitLab deploy token
kubectl create secret docker-registry gitlab-registry \
--docker-server=registry.gitlab.com \
--docker-username=<deploy-token-username> \
--docker-password=<deploy-token-password> \
-n productionGet a deploy token from: Settings → Repository → Deploy tokens.
Then use it in your Kubernetes deployment:
spec:
template:
spec:
imagePullSecrets:
- name: gitlab-registry
containers:
- name: app
image: registry.gitlab.com/your-group/your-project:latestStep 6: Add Environments and Manual Approvals
GitLab has first-class environment support with manual gates:
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- ./scripts/deploy.sh staging
environment:
name: staging
url: https://staging.myapp.com
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
script:
- ./scripts/deploy.sh production
environment:
name: production
url: https://myapp.com
when: manual # requires a human to click "deploy"
allow_failure: false
rules:
- if: '$CI_COMMIT_BRANCH == "main"'With when: manual, the job appears in the pipeline but won't run until someone clicks the play button in the GitLab UI. This gives you staging → production with a human gate.
Step 7: Caching and Artifacts
Caching (speed up dependency installation)
# Cache node_modules based on lockfile
cache:
key:
files:
- package-lock.json
paths:
- .npm/
# Use the cache
install:
stage: .pre
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths:
- node_modules/ # pass to subsequent jobs
expire_in: 1 hourArtifacts (pass files between jobs)
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy:
stage: deploy
needs:
- job: build
artifacts: true # download build artifacts
script:
- ls dist/ # files from build job are here
- ./deploy.shStep 8: Pipeline Rules (Control When Jobs Run)
# Only run tests on MRs and main branch
test:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
# Only deploy on main branch, not on MRs
deploy:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: on_success
- when: never
# Skip if commit message contains [skip ci]
.default-rules: &default-rules
rules:
- if: '$CI_COMMIT_MESSAGE =~ /\[skip ci\]/'
when: never
- when: on_successControl which pipeline runs at all
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' # MR pipelines
- if: '$CI_COMMIT_BRANCH == "main"' # main branch pushes
- if: '$CI_COMMIT_TAG' # version tagsStep 9: Notifications
Send Slack notifications when the pipeline fails:
notify-failure:
stage: .post
image: curlimages/curl:latest
script:
- |
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
--data "{
\"text\": \":x: Pipeline failed on <$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID|#$CI_PIPELINE_ID>\",
\"channel\": \"#deployments\"
}"
when: on_failure
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Complete Working Example
Here's a minimal but real pipeline you can use immediately:
# .gitlab-ci.yml — production-ready template
image: node:20-alpine
stages:
- test
- build
- deploy
variables:
REGISTRY: $CI_REGISTRY_IMAGE
TAG: $CI_COMMIT_SHORT_SHA
test:
stage: test
script:
- npm ci
- npm test
cache:
key: { files: [package-lock.json] }
paths: [.npm/]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
build:
stage: build
image: docker:24.0
services: [docker:24.0-dind]
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $REGISTRY:$TAG -t $REGISTRY:latest .
- docker push $REGISTRY:$TAG
- docker push $REGISTRY:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- echo "$KUBECONFIG_DATA" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
script:
- kubectl set image deployment/my-app app=$REGISTRY:$TAG -n production
- kubectl rollout status deployment/my-app -n production --timeout=120s
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manualLearn More
Want to master GitLab CI/CD with real hands-on environments? KodeKloud's CI/CD courses cover GitLab, GitHub Actions, Jenkins, and ArgoCD with real pipeline exercises — not just documentation walkthroughs.
If you're looking for a cloud environment to deploy to, DigitalOcean's App Platform connects directly to GitLab repos and handles deployment automatically — great for getting started without managing your own Kubernetes cluster.
Summary
GitLab CI/CD gives you a complete pipeline platform in one tool:
.gitlab-ci.yml— single file, defines everything- Runners — shared (free) or self-hosted
- Stages — lint → test → build → push → deploy
- Variables — secrets in the UI, built-in vars everywhere
- Container Registry — built-in, free, no Docker Hub needed
- Environments — staging and production with manual gates
- Cache + Artifacts — fast pipelines, files between stages
- Rules — control when every job runs
Start with the minimal template above and add stages as your project grows.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Ansible vs Terraform: Which One Should You Use? (2026)
Ansible and Terraform are both called 'IaC tools' but they solve completely different problems. Here's when to use each — and when to use both.
Build a Kubernetes Cluster with kubeadm from Scratch (2026)
Step-by-step guide to building a real multi-node Kubernetes cluster using kubeadm — no managed services, no shortcuts.
GitLab CI Pipeline Failed: Debug and Fix It
Debug a failed GitLab CI pipeline step by step. Fix YAML errors, missing variables, runner problems, Docker-in-Docker failures, and job dependency issues.