GitLab CI pipelines from scratch — Real‑World Case Study — Practical Guide (Apr 17, 2026)
GitLab CI pipelines from scratch — Real‑World Case Study
Level: Intermediate
As of 17 April 2026, GitLab version 16.0 and later.
Introduction
Continuous Integration (CI) pipelines are fundamental to modern software development. GitLab CI/CD provides an integrated, flexible system for automating build, test, and deployment workflows directly within your Git repository. This article walks through creating a robust GitLab CI pipeline from scratch based on a real-world scenario, targeting intermediate developers familiar with basic Git and CI concepts.
We will focus on GitLab 16.x stable features, avoiding experimental flags or preview functionality.
Prerequisites
- Access to a GitLab instance version 16.0 or newer (Cloud or self-managed)
- A project repository with basic code (for example, a multi-language microservice app)
- Familiarity with GitLab’s UI and Git workflows
- Runner(s) registered and configured (shared runners are sufficient for initial steps)
- Basic knowledge of Docker (optional but highly recommended)
- Working knowledge of YAML syntax (for
.gitlab-ci.ymlfiles)
Hands-on Steps
Step 1: Define your pipeline stages
Start by outlining the phases your pipeline will have — typically:
build— Compile or create artefactstest— Execute unit and integration testspackage— Prepare deliverablesdeploy— Deploy to an environment (staging, production)
Minimal .gitlab-ci.yml snippet defining stages:
stages:
- build
- test
- package
- deploy
Step 2: Write your first job
Each stage contains jobs. Begin with a simple build job that runs on any runner.
build_job:
stage: build
script:
- echo "Compiling the application..."
- # Insert build commands, e.g. mvn package, npm run build, etc.
artifacts:
paths:
- build/
expire_in: 1 hour
Artifacts preserve outputs between stages. Adjust paths as per your project structure.
Step 3: Add a test job
Testing is vital. Introduce a job in the test stage to validate your code.
test_job:
stage: test
script:
- echo "Running tests..."
- # Example for JavaScript: npm test
needs:
- build_job
allow_failure: false
coverage: '/Coverage:s+d+%/' # Optional, if you generate coverage reports
The needs keyword optimises pipeline concurrency by allowing dependent jobs to run sooner. Use it judiciously for complex DAGs.
Step 4: Package and deploy steps
Your package job bundles executables, and deploy jobs will optionally push code to environments.
package_job:
stage: package
script:
- echo "Packaging application..."
- # Package commands, e.g. zip, docker build, etc.
needs:
- test_job
artifacts:
paths:
- dist/
expire_in: 2 hours
deploy_staging:
stage: deploy
script:
- echo "Deploying to staging server..."
- # Deployment commands, like SSH or Kubernetes kubectl apply
needs:
- package_job
environment:
name: staging
url: https://staging.example.com
only:
- main # or your production branch
Use the environment keyword for tracking, review apps, and auto-stop features.
Step 5: Implement CI variables and secrets management
Manage sensitive data like API keys or deploy tokens via GitLab CI/CD variables in your project settings rather than hardcoding. Example referencing a variable in a script:
deploy_staging:
stage: deploy
script:
- echo "Deploying with secret key"
- deploy-scripts --key "$DEPLOY_KEY"
variables:
DEPLOY_KEY: $DEPLOY_KEY # Retrieved from CI/CD settings
Common pitfalls
- Not locking runner tags: Specify
tagsin jobs if using custom runners; jobs will stall otherwise. - Large artifacts: Avoid storing extremely large binaries; this can slow down your pipeline and consume runner storage.
- Missing cache usage: Use
cache:to speed up dependency installs; not caching may slow subsequent builds. - Infinite loops: Avoid triggering pipelines on branch/tag patterns that cause recursive runs (e.g. Docker image pushes triggering pipelines).
- Ignoring pipeline status: Failing pipelines should block merge requests; enforce this via branch protection rules.
Validation
Once your .gitlab-ci.yml is committed, GitLab automatically parses and validates the syntax on push:
- Check pipeline status in the “CI/CD > Pipelines” page.
- Expand the job logs for real-time output and error tracking.
- Use GitLab’s built-in CI Lint tool at
https://gitlab.com/-/ci/lintto pre-validate your config before pushing.
Confirm environment URLs are correct and access logs during deployment. Configure notifications to alert failures promptly.
Checklist / TL;DR
- Define clear, logical stages to represent your workflow
- Create succinct jobs with meaningful names and scripts
- Use
artifactsandcachewisely to preserve and speed up state - Leverage
needsfor parallel execution and speed - Secure secrets with CI/CD variables; never commit them in code
- Use environment keyword to manage deployment lifecycle
- Validate and test pipelines often using GitLab’s lint and UI status
- Plan runners capacity — consider shared vs. private runners depending on build load and security
- Use branch rules and validations to enforce code quality gates