DevOpsClicks
← Home
🦊 Complete CI/CD Platform

GitLab CI/CD Complete Guide

From .gitlab-ci.yml basics to production pipelines with security scanning, Docker builds, and Kubernetes deployments for every tech stack.

12
Chapters
30+
Pipelines
100%
Free
01🦊

Introduction to GitLab

All-in-One DevOps Platform

GitLab is a complete DevOps platform in a single application — Git repos, CI/CD pipelines, container registry, security scanning, project management, and monitoring. Unlike Jenkins (CI/CD only) or GitHub (repos + basic actions), GitLab has EVERYTHING built-in. Many enterprises choose GitLab because one tool replaces 5-6 separate tools.
GitLab vs GitHub vs Jenkins
FeatureGitLabGitHubJenkins
CI/CDBuilt-in (.gitlab-ci.yml)GitHub Actions (.yml)Jenkinsfile (Groovy)
Container RegistryBuilt-inGitHub PackagesExternal (ECR/ACR)
Security ScanningBuilt-in (SAST, DAST, SCA)Third-party ActionsPlugins
Self-HostedYes (GitLab CE/EE)GitHub EnterpriseAlways self-hosted
Project BoardsBuilt-inGitHub ProjectsNone
Best ForEnterprise all-in-oneOpen source + communityMax customization
02📝

Pipeline Basics

Your First .gitlab-ci.yml

Every GitLab pipeline is defined in a file called .gitlab-ci.yml at the root of your repository. When you push code, GitLab reads this file and executes the pipeline automatically. Think of it as a recipe — stages are chapters, jobs are steps within each chapter.
Simplest Pipeline
GITLAB-CI.YML# .gitlab-ci.yml — lives in repo root stages: # Order of execution - build - test - deploy build-job: # Job name (you choose) stage: build # Which stage this belongs to script: # Commands to run - echo "Building the app..." - mvn clean package test-job: stage: test script: - echo "Running tests..." - mvn test deploy-job: stage: deploy script: - echo "Deploying to production..." only: - main # Only run on main branch
Pipeline Hierarchy
LevelContainsExample
PipelineStagesOne push = one pipeline run
StageJobsbuild, test, security, deploy
JobScripts + Configbuild-java, test-unit, deploy-k8s
ScriptShell commandsmvn package, npm ci, docker build
💡 Key Rule

Jobs in the SAME stage run in parallel. Stages run in order. So all test jobs run simultaneously, but deploy only starts after ALL test jobs pass.

03🚀

Advanced Pipeline Features

Rules, Needs, Cache & Artifacts

Production pipelines need conditional execution, dependency management, caching, and artifact passing between jobs.
rules — Modern Conditional Execution
YAMLdeploy-production: stage: deploy script: kubectl apply -f k8s/ rules: - if: $CI_COMMIT_BRANCH == "main" # Only on main branch when: manual # Require manual click - if: $CI_MERGE_REQUEST_ID # On merge requests when: never # Never deploy from MR # rules replaces the older only/except syntax # Common variables: # $CI_COMMIT_BRANCH → branch name # $CI_COMMIT_TAG → tag name (v1.0.0) # $CI_MERGE_REQUEST_ID → MR number # $CI_PIPELINE_SOURCE → push, merge_request_event, schedule
artifacts — Pass Files Between Jobs
YAMLbuild-app: stage: build script: - mvn clean package -DskipTests artifacts: paths: - target/*.jar # Save the JAR file expire_in: 1 hour # Auto-delete after 1 hour deploy-app: stage: deploy script: - ls target/ # JAR is available here! - cp target/app.jar /opt/app/ needs: [build-app] # Only depends on build-app, not entire stage
cache — Speed Up Builds
YAML# Cache downloads dependencies so they don't re-download every build build-java: stage: build cache: key: ${CI_COMMIT_REF_SLUG} # Cache per branch paths: - .m2/repository/ # Maven dependencies variables: MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" script: - mvn clean package build-node: stage: build cache: key: files: [package-lock.json] # Cache busts when lock file changes paths: - node_modules/ script: - npm ci - npm run build
04🔐

Variables & Secrets

Configure Pipelines Securely

Variables store configuration values and secrets. Define them in .gitlab-ci.yml (non-sensitive) or in GitLab UI Settings (passwords, tokens, API keys).
YAML# In .gitlab-ci.yml — non-sensitive values variables: APP_NAME: "order-service" DOCKER_REGISTRY: "registry.gitlab.com/mygroup/myproject" JAVA_VERSION: "17" # In GitLab UI → Settings → CI/CD → Variables: # DB_PASSWORD = s3cr3t (masked + protected) # AWS_ACCESS_KEY = AKIA... (masked + protected) # KUBECONFIG_FILE = <file> (type: File) # Usage in jobs: build: script: - echo "Building $APP_NAME" # From .gitlab-ci.yml - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY # Built-in! - echo $DB_PASSWORD # From UI (masked in logs)
💡 Built-in Variables

GitLab provides 50+ predefined variables: $CI_COMMIT_SHA (commit hash), $CI_REGISTRY (container registry URL), $CI_PROJECT_NAME, $CI_PIPELINE_ID, $CI_JOB_TOKEN (auto-generated token). You never need to configure registry credentials manually!

05🔨

CI Pipeline Examples

Build Every Stack — Java, Node, React, Python, Flutter

Here are production-ready CI pipelines for every major tech stack. Each shows: what the build command does, what artifact it produces, and where to find the output.
Java Spring Boot (Maven)
YAML# Build: mvn clean package # Produces: target/app-1.0.0.jar (fat JAR with embedded Tomcat) # Size: ~30-80 MB build-java: stage: build image: maven:3.9-eclipse-temurin-17 cache: paths: [.m2/repository] variables: MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" script: - mvn clean package -DskipTests artifacts: paths: [target/*.jar] test-java: stage: test image: maven:3.9-eclipse-temurin-17 script: - mvn test artifacts: reports: junit: target/surefire-reports/TEST-*.xml
Node.js (Express/NestJS)
YAML# Build: npm ci (install exact deps from lock file) # Produces: node_modules/ + source code (no separate artifact) # For TypeScript: npm run build → produces dist/ folder build-node: stage: build image: node:20-alpine cache: key: { files: [package-lock.json] } paths: [node_modules/] script: - npm ci - npm run build # TypeScript → JavaScript in dist/ artifacts: paths: [dist/, node_modules/]
React Frontend
YAML# Build: npm run build # Produces: build/ folder with static HTML, JS, CSS files # Size: ~2-10 MB (optimized, minified) # Deploy to: Nginx, S3 + CloudFront, Vercel build-react: stage: build image: node:20-alpine script: - npm ci - npm run build artifacts: paths: [build/]
Python (Django/Flask)
YAML# Build: pip install (no compilation for pure Python) # Produces: installed packages in venv # Deploy: Docker image with requirements installed build-python: stage: build image: python:3.12-slim script: - pip install -r requirements.txt - python -m pytest tests/ --junitxml=report.xml artifacts: reports: junit: report.xml
Flutter Mobile App
YAML# Build: flutter build apk (Android) / flutter build ios # Produces: build/app/outputs/flutter-apk/app-release.apk # Size: ~15-50 MB build-flutter: stage: build image: cirrusci/flutter:latest script: - flutter pub get - flutter test - flutter build apk --release artifacts: paths: - build/app/outputs/flutter-apk/app-release.apk
06🐳

Docker Build & Push

Containerize and Push to Registry

After building your app, you package it into a Docker image and push it to a container registry. GitLab has a built-in container registry — no external setup needed.
Build → Push → Deploy Flow
FLOW# The full journey of your code: # 1. Source code → Build (mvn package / npm build) # 2. Build artifact → Docker image (COPY jar/dist into container) # 3. Docker image → Registry (push to GitLab/ECR/ACR) # 4. Registry → Kubernetes (pull image and run)
Spring Boot → Docker → GitLab Registry
GITLAB-CI.YMLstages: [build, docker, deploy] build: stage: build image: maven:3.9-eclipse-temurin-17 script: mvn clean package -DskipTests artifacts: paths: [target/*.jar] docker-build: stage: docker image: docker:24 services: [docker:24-dind] # Docker-in-Docker variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG - echo "Image pushed: $IMAGE_TAG" # No credentials needed — $CI_REGISTRY_* are built-in!
Dockerfiles for Each Stack
DOCKERFILES# ═══ Spring Boot Dockerfile ═══ FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # Final image: ~180 MB # ═══ React Dockerfile ═══ FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/build /usr/share/nginx/html # Final image: ~25 MB # ═══ Node.js Dockerfile ═══ FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY dist/ ./dist/ EXPOSE 3000 CMD ["node", "dist/server.js"] # Final image: ~150 MB # ═══ Python Dockerfile ═══ FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
Push to External Registries (ECR/ACR)
YAML# Push to AWS ECR docker-push-ecr: stage: docker image: docker:24 services: [docker:24-dind] before_script: - apk add --no-cache aws-cli - aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY script: - docker build -t $ECR_REGISTRY/order-service:$CI_COMMIT_SHORT_SHA . - docker push $ECR_REGISTRY/order-service:$CI_COMMIT_SHORT_SHA # Push to Azure ACR docker-push-acr: before_script: - docker login $ACR_REGISTRY -u $ACR_USERNAME -p $ACR_PASSWORD
07🛡️

Security Scanning Stage

SonarQube, Fortify & Black Duck

Security scanning catches vulnerabilities BEFORE code reaches production. In enterprise CI/CD, every pipeline has security stages between build and deploy. The three main types: SAST (code analysis), SCA (dependency vulnerabilities), and DAST (runtime testing).
Security Scanning Types
🔍
SAST (Static Analysis)
Scans your SOURCE CODE for vulnerabilities — SQL injection, XSS, hardcoded secrets. Runs WITHOUT executing the app. Tools: SonarQube, Fortify, Checkmarx.
📦
SCA (Software Composition)
Scans your DEPENDENCIES (libraries) for known vulnerabilities. "You're using log4j 2.14 which has a critical CVE." Tools: Black Duck, Snyk, Trivy.
🌐
DAST (Dynamic Analysis)
Scans the RUNNING APPLICATION by sending malicious requests. Finds runtime vulnerabilities. Tools: OWASP ZAP, Burp Suite.
🐳
Container Scanning
Scans your Docker IMAGE for OS-level vulnerabilities. "Alpine 3.14 has CVE-2023-xxxx." Tools: Trivy, Grype, Prisma Cloud.
SonarQube — Code Quality & SAST
YAMLsonarqube-scan: stage: security image: sonarsource/sonar-scanner-cli:latest variables: SONAR_HOST_URL: "https://sonar.company.com" SONAR_TOKEN: $SONAR_TOKEN # Set in CI/CD Variables script: - sonar-scanner -Dsonar.projectKey=$CI_PROJECT_NAME -Dsonar.sources=src/ -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN -Dsonar.qualitygate.wait=true # FAIL pipeline if quality gate fails allow_failure: false # Block deploy if scan fails # SonarQube checks: # - Code smells (bad practices) # - Bugs (potential errors) # - Vulnerabilities (security issues) # - Code coverage (% of code tested) # - Duplicated code
Fortify — Enterprise SAST
YAMLfortify-scan: stage: security image: fortify-scanner:latest script: - sourceanalyzer -b $CI_PROJECT_NAME -clean - sourceanalyzer -b $CI_PROJECT_NAME src/ - sourceanalyzer -b $CI_PROJECT_NAME -scan -f results.fpr - fortifyclient uploadFPR -file results.fpr -url $FORTIFY_SSC_URL artifacts: paths: [results.fpr] allow_failure: false
Black Duck — SCA (Dependency Scanning)
YAMLblackduck-scan: stage: security image: blackducksoftware/detect:latest script: - bash <(curl -s https://detect.synopsys.com/detect8.sh) --blackduck.url=$BLACKDUCK_URL --blackduck.api.token=$BLACKDUCK_TOKEN --detect.project.name=$CI_PROJECT_NAME --detect.project.version.name=$CI_COMMIT_REF_NAME --detect.policy.check.fail.on.severities=CRITICAL,HIGH allow_failure: false # Black Duck checks your dependencies: # - Known vulnerabilities (CVEs) # - License compliance (GPL, MIT, Apache) # - Outdated components
Trivy — Container Image Scanning
YAMLtrivy-scan: stage: security image: aquasec/trivy:latest script: - trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA allow_failure: false # Exit code 1 = found critical/high vulns → pipeline fails
Complete Pipeline with Security
PIPELINE FLOWstages: [build, test, security, docker, deploy] # Stage 1: Build build: { stage: build, script: "mvn clean package" } # Stage 2: Test test: { stage: test, script: "mvn test" } # Stage 3: Security (all run in parallel) sonarqube: { stage: security, script: "sonar-scanner ..." } blackduck: { stage: security, script: "detect.sh ..." } fortify: { stage: security, script: "sourceanalyzer ..." } # Stage 4: Docker (only if all security passes) docker-push: { stage: docker, script: "docker build && push" } trivy-scan: { stage: docker, script: "trivy image ..." } # Stage 5: Deploy (manual approval for production) deploy-prod: { stage: deploy, script: "kubectl apply ...", when: manual }
💡 Shift Left

\"Shift Left\" means moving security scanning EARLIER in the pipeline — catch issues during development, not in production. The earlier you find a vulnerability, the cheaper it is to fix. Security in CI is 100x cheaper than a breach in production.

08🚀

Deploy to Kubernetes

EKS, AKS, GKE Deployment

After building the Docker image and passing security scans, deploy it to Kubernetes. The pattern is the same for AWS EKS, Azure AKS, and Google GKE — only the authentication differs.
Deploy to AWS EKS
YAMLdeploy-eks: stage: deploy image: bitnami/kubectl:latest before_script: - aws eks update-kubeconfig --name my-cluster --region ap-south-1 script: - kubectl set image deployment/order-service app=$ECR_REGISTRY/order-service:$CI_COMMIT_SHORT_SHA -n production - kubectl rollout status deployment/order-service -n production --timeout=120s rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
Deploy to Azure AKS
YAMLdeploy-aks: stage: deploy image: bitnami/kubectl:latest before_script: - az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET -t $AZURE_TENANT - az aks get-credentials --name my-aks --resource-group my-rg script: - kubectl apply -f k8s/ rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
09🌍

Environments & Approvals

Multi-Stage Deployments

GitLab Environments track where your code is deployed. Each environment can have approval rules, auto-stop schedules, and deployment history.
YAMLdeploy-staging: stage: deploy environment: name: staging url: https://staging.myapp.com script: kubectl apply -f k8s/staging/ rules: - if: $CI_COMMIT_BRANCH == "develop" deploy-production: stage: deploy environment: name: production url: https://myapp.com script: kubectl apply -f k8s/production/ rules: - if: $CI_COMMIT_BRANCH == "main" when: manual # Requires manual approval click # In GitLab Premium: Settings → CI/CD → Protected Environments # → Add approvers for production environment
10🏃

GitLab Runners

Execute Your Pipelines

Runners are the machines that actually execute your CI/CD jobs. GitLab provides shared runners (free, limited minutes) or you install your own (self-hosted, unlimited).
Runner TypeWhere It RunsBest For
Shared (SaaS)GitLab.com cloudSmall projects, getting started (400 min/month free)
Group RunnerYour server, shared across groupTeam-level, shared by all projects in a group
Project RunnerYour server, specific projectSpecialized builds (GPU, large disk, private network)
TERMINAL# Install GitLab Runner on your server curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash sudo apt install gitlab-runner # Register runner with your GitLab instance sudo gitlab-runner register # URL: https://gitlab.com/ (or your self-hosted URL) # Token: from Settings → CI/CD → Runners # Executor: docker (recommended) # Default image: alpine:latest # Use Docker executor — every job runs in a fresh container # Clean, reproducible, isolated builds
11

Advanced Features

Includes, Child Pipelines & Templates

Enterprise GitLab uses includes to share pipeline code, child pipelines for complex workflows, and templates for standardization.
include — Reuse Pipeline Code
YAML# .gitlab-ci.yml — simple, calls shared templates include: # From another project - project: 'devops/pipeline-templates' file: '/templates/docker-build.yml' # From a URL - remote: 'https://company.com/templates/security.yml' # From same repo - local: '/ci/deploy.yml' stages: [build, test, security, docker, deploy] # All jobs from included files are merged into this pipeline # Change the template once → all 50 projects get the update
12💼

Interview Questions

GitLab CI/CD Q&A

What is .gitlab-ci.yml?
Pipeline definition file at repo root. Defines stages, jobs, scripts, rules, artifacts, and variables. GitLab reads it on every push and executes the pipeline automatically.
Stages vs Jobs?
Stages define ORDER (build → test → deploy). Jobs define WORK (build-java, test-unit). Jobs in same stage run in parallel. Next stage waits for all jobs to pass.
artifacts vs cache?
Artifacts pass files between jobs (build JAR → deploy JAR). Cache speeds up builds by saving dependencies between pipeline runs (node_modules, .m2/repository).
What are GitLab Runners?
Machines that execute pipeline jobs. Shared (GitLab cloud), Group (shared in org), Project (dedicated). Docker executor = clean container per job.
SAST vs SCA vs DAST?
SAST: scans source code (SonarQube, Fortify). SCA: scans dependencies (Black Duck, Snyk). DAST: scans running app (ZAP). All three are needed for complete security.
rules vs only/except?
rules is modern, more powerful (if/when/exists conditions). only/except is legacy. Always use rules for new pipelines.
How to deploy to K8s?
Build → Docker push to registry → kubectl set image or kubectl apply. Use environment: for tracking. Add when: manual for production approval gate.
GitLab vs Jenkins?
GitLab: all-in-one (repos + CI/CD + registry + security), YAML pipelines. Jenkins: CI/CD only, 1800+ plugins, Groovy pipelines. GitLab is simpler, Jenkins is more customizable.