GitLab CI Best Practices
Learn GitLab CI best practices for faster pipelines, better caching, DAG execution with needs, reusable templates, rules-based workflows, protected deployments, and pipeline performance optimization.
A GitLab CI/CD pipeline can either feel like a productivity multiplier or a daily source of friction. Fast, readable, reliable pipelines give teams confidence to ship. Slow, repetitive, fragile pipelines create long feedback loops, hidden waste, and risky release habits. That is why GitLab CI best practices matter. Good pipeline design is not just about valid YAML. It is about reducing developer wait time, making failures obvious, protecting sensitive operations, and keeping CI logic maintainable as the codebase grows.
This tutorial covers practical GitLab pipeline optimization principles you can apply whether you manage one application or dozens. The goal is not to make the most complex pipeline possible. The goal is to build efficient pipelines that are fast, clear, secure, and scalable.
Principle 1: Keep Pipelines Fast
Speed is the first pipeline feature users notice. If basic feedback takes too long, developers wait, context-switch, or skip checks locally. Fast pipelines improve both delivery flow and code quality.
Ways to keep GitLab CI pipelines fast include:
- run independent jobs in parallel
- use smaller base images when possible
- avoid unnecessary package installation during every job
- separate quick validation from slow integration work
- use
needsto start downstream jobs early
For example, this pipeline is serialized more than necessary:
stages:
- build
- test
- deploy
If build, lint, and unit tests do not all depend on each other, you can often redesign the graph so jobs start sooner.
Also pay attention to Docker images. Pulling large images repeatedly is expensive. If a job only needs Node.js, use a focused Node image instead of a general-purpose image packed with tools you do not need.
Principle 2: Fail Fast
Not all failures are equal. If linting or unit tests fail in 45 seconds, there is no reason to wait 15 minutes for a slow deployment package step to tell you the pipeline failed.
Put the fastest and most failure-prone feedback early:
- lint
- formatting checks
- unit tests
- config validation
Then run heavier jobs such as long integration suites, container builds, and deployment jobs after the basics pass.
This is both a technical and human optimization. The earlier the pipeline tells a developer something is wrong, the less time is wasted.
A strong fail-fast pattern is:
stages:
- validate
- test
- package
- deploy
Even better, combine this with DAG-style needs so jobs do not wait on unrelated stage barriers.
Principle 3: Use Caching Effectively
Caching is one of the most important GitLab pipeline optimization tools. A good cache prevents repeated downloading and setup work across jobs and pipeline runs.
Node.js caching
cache:
key:
files:
- package-lock.json
paths:
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
Python caching
cache:
key:
files:
- requirements.txt
paths:
- .pip-cache/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
Maven caching
cache:
key: maven-$CI_COMMIT_REF_SLUG
paths:
- .m2/repository/
The main best practice is to cache dependency directories or package manager caches, not random build outputs. Use cache for things that improve performance and can be safely recreated.
Also remember that cache keys matter. A lock-file based key is much better than one permanent key that never changes because it refreshes naturally when dependencies change.
Principle 4: Use Artifacts Sparingly
Artifacts are essential when one job must pass real outputs to another job, but they are often overused. Huge artifact archives increase storage usage and slow job downloads.
Good artifact practice means:
- store only what downstream jobs actually need
- set sensible expiration times
- avoid archiving huge directories by default
- prefer reports for structured results like JUnit or coverage
Bad pattern:
artifacts:
paths:
- .
Good pattern:
artifacts:
paths:
- dist/
expire_in: 1 week
If the next job only needs dist/, do not archive the entire workspace.
Principle 5: DRY Pipelines with YAML Anchors and Aliases
Repeated CI configuration becomes hard to maintain. If five jobs all install the same dependencies, use YAML anchors and aliases to define the shared behavior once.
Example:
.default_node_job: &default_node_job
image: node:20
before_script:
- npm ci
lint:
<<: *default_node_job
stage: validate
script:
- npm run lint
test:
<<: *default_node_job
stage: test
script:
- npm test
This keeps the file smaller and reduces copy-paste errors.
Anchors are especially useful for:
- base job definitions
- shared
before_script - common image and cache settings
- repeated rules blocks
Principle 6: Use include to Share Pipeline Templates
YAML anchors help inside one file. GitLab include helps across files and projects.
Use include when:
- multiple repositories share the same pipeline logic
- a platform team maintains standard CI templates
- compliance or security jobs must be reused consistently
- the root
.gitlab-ci.ymlis getting too large
Example:
include:
- project: my-group/pipeline-library
ref: main
file: /templates/node-service.yml
This is one of the best GitLab CI templates patterns because it centralizes maintenance. When shared logic improves, multiple projects benefit.
The trade-off is governance: shared templates should be versioned carefully so a breaking change in the template does not surprise many repositories at once.
Principle 7: Keep Secrets Out of the Pipeline Definition
One of the most basic GitLab CI best practices is also one of the most important: never hardcode secrets in .gitlab-ci.yml.
Bad example:
variables:
AWS_SECRET_ACCESS_KEY: abc123supersecret
Correct approach:
- store secrets in GitLab CI/CD variables
- mark them as masked and protected where appropriate
- scope production secrets to protected branches or tags
- prefer cloud federation patterns like OIDC over long-lived keys
This matters because .gitlab-ci.yml is source code. If you put a secret there, it enters Git history and becomes much harder to contain.
Principle 8: Use needs for DAG Pipelines
Traditional stage ordering forces all jobs in one stage to finish before the next stage begins. That is simple, but often slower than necessary.
The needs keyword lets you build a DAG pipeline, where jobs start as soon as their specific dependencies are ready.
Example:
stages:
- validate
- test
- package
lint:
stage: validate
script: npm run lint
unit_tests:
stage: test
script: npm test
build_image:
stage: package
needs:
- unit_tests
script: docker build -t myapp .
With needs, build_image does not wait for every job in previous stages. It waits only for the job it actually depends on.
This is one of the biggest improvements you can make to pipeline speed in GitLab.
Principle 9: Use rules Instead of Only only/except
Older pipelines often rely on only and except. They still exist, but rules is generally more expressive and easier to understand in modern GitLab CI/CD.
With rules, you can control job creation based on:
- branch names
- tags
- merge requests
- file changes
- pipeline source
- variables
Example:
deploy_production:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- when: never
script:
- ./deploy.sh production
This is clearer than trying to combine only, except, and other conditions across multiple jobs.
Principle 10: Protect Production with Manual Gates and Protected Environments
Not every deployment should be automatic. For sensitive environments, introduce deliberate control.
Good production protection patterns include:
when: manualfor release approval steps- protected branches controlling who can trigger a pipeline
- protected environments restricting who can deploy
- separate variables for production secrets
- post-deploy smoke checks
Example:
deploy_production:
stage: deploy
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
script:
- ./deploy.sh production
This does not mean every environment needs manual approval. It means the highest-risk environments should not be too easy to change by accident.
Common Mistakes to Avoid
Even experienced teams fall into predictable CI pitfalls.
No caching
If every job re-downloads dependencies from scratch, the pipeline will stay slow.
Using latest images without pinning
latest looks convenient, but it hurts reproducibility. Pin images to known versions:
image: node:20.16.0
Too many stages
Some pipelines create a new stage for every conceptual step. That often slows everything down because stage barriers multiply. Prefer fewer stages with clearer DAG dependencies.
Artifact sprawl
Saving the whole workspace from every job wastes time and storage.
Copy-paste YAML everywhere
If every repo has nearly identical pipeline logic, invest in templates and includes.
Hidden secrets in logs
Even if secrets are stored correctly, careless commands can print them. Review job scripts for accidental echoing of sensitive values.
Measuring Pipeline Performance with GitLab CI Analytics
Optimization works best when you measure before and after. GitLab CI provides analytics and job timing data that help you answer questions such as:
- Which jobs take the longest?
- Which stages are bottlenecks?
- Are pipeline durations improving over time?
- Are there flaky jobs that frequently retry or fail?
Use pipeline analytics, job duration views, and merge request timing trends to guide improvements. Do not optimize blindly. Focus first on the jobs that consume the most time or fail most often.
For example, if a pipeline takes 18 minutes and 10 of those minutes come from dependency installation, caching may have a larger impact than any YAML refactor. If the problem is a serialized deployment graph, needs may be the best fix.
Refactored Pipeline Example: Before vs After
Here is a deliberately inefficient pipeline.
Before
stages:
- install
- lint
- test
- build
- package
- deploy
install:
stage: install
image: node:latest
script:
- npm ci
lint:
stage: lint
image: node:latest
script:
- npm ci
- npm run lint
test:
stage: test
image: node:latest
script:
- npm ci
- npm test
build:
stage: build
image: node:latest
script:
- npm ci
- npm run build
artifacts:
paths:
- .
package:
stage: package
script:
- docker build -t myapp .
deploy:
stage: deploy
script:
- ./deploy.sh
Problems:
node:latestis unpinnednpm cirepeats in every job without caching- artifacts save the entire workspace
- stage separation is overly strict
- no rules, no environment protection, no reusable anchors
After
stages:
- validate
- test
- package
- deploy
.default_node_job: &default_node_job
image: node:20.16.0
cache:
key:
files:
- package-lock.json
paths:
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
before_script:
- npm ci
lint:
<<: *default_node_job
stage: validate
script:
- npm run lint
unit_tests:
<<: *default_node_job
stage: test
script:
- npm test
build_app:
<<: *default_node_job
stage: package
needs:
- unit_tests
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
build_image:
stage: package
image: docker:27
needs:
- build_app
script:
- docker build -t myapp:$CI_COMMIT_SHA .
deploy_production:
stage: deploy
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- when: never
script:
- ./deploy.sh production
Why the refactor is better:
- fewer stages and clearer execution flow
- pinned image version for consistency
- dependency caching to improve speed
- YAML anchor to remove duplication
- smaller artifact footprint
needsfor faster package steps- rules-driven protected production deployment
That is what GitLab CI best practices look like in action: not flashy YAML tricks, but clearer and faster automation.
Final Takeaway
The best GitLab pipelines are fast, focused, and maintainable. Keep feedback loops short, cache expensive dependency work, use artifacts deliberately, reduce duplication with anchors and includes, prefer rules, accelerate flow with needs, and protect production with manual gates and protected environments. If you apply those principles consistently, your GitLab CI/CD setup becomes easier to trust and easier to scale.
Pipeline optimization is rarely about one magic keyword. It is about many small improvements that remove waste from the delivery path.
Knowledge Check
Question 1: Pipeline Speed
Which GitLab CI feature is especially useful for starting jobs as soon as their real dependencies are ready instead of waiting for an entire previous stage to finish?
Question 2: Secrets Management
What is the best practice for handling secrets in GitLab CI/CD?
Question 3: DRY Pipelines
Why would you use YAML anchors and the include keyword in GitLab CI/CD?