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.
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 Keeps Failing? Here's How to Debug and Fix It
GitLab CI pipelines fail for dozens of reasons. This guide walks through the most common errors — from Docker-in-Docker issues to missing variables — and shows you exactly how to fix them.