DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Cicd Tutorial

Introduction to CI/CD
Getting Started with GitLab CI/CD
Stages, Jobs, Artifacts & Cache
Variables, Secrets and Environments in GitLab CI
GitLab Runners
GitLab CI Pipeline Rules
GitLab CI Environments
GitLab CI Docker Workflows
GitLab CI Testing
GitLab CI Security Scanning
GitLab CI Deploy to AWS
GitLab CI Multi-Project Pipelines
GitLab CI Best Practices

GitLab CI Best Practices

PreviousPrev

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 needs to 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.yml is 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: manual for 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:latest is unpinned
  • npm ci repeats 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
  • needs for 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

Exercise

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?

Exercise

Question 2: Secrets Management

What is the best practice for handling secrets in GitLab CI/CD?

Exercise

Question 3: DRY Pipelines

Why would you use YAML anchors and the include keyword in GitLab CI/CD?

PreviousPrev

Continue Learning

GitLab CI Security Scanning

Learn GitLab CI security scanning with SAST, DAST, secret detection, dependency scanning, container scanning, security reports, and DevSecOps best practices for secure pipelines.

12 min read·Medium

GitLab CI Deploy to AWS

Learn GitLab CI deploy AWS patterns including CI/CD variables, AWS OIDC, S3 deployments, EC2 SSH deploys, ECR image pushes, ECS service updates, and Terraform automation.

14 min read·Medium

GitLab CI Multi-Project Pipelines

Learn GitLab multi-project pipelines, downstream triggers, child pipelines, include strategies, trigger API usage, dynamic child pipelines, and cross-project dependencies in GitLab CI/CD.

10 min read·Medium

Explore Related Topics

Do

Docker Tutorials

Build and push Docker images in your pipelines

Gi

Git Tutorials

Trigger CI/CD pipelines from Git events

Try the Tool

YAML Validator

Validate GitHub Actions and CI/CD pipeline YAML files.

Cron Parser

Build and understand scheduled workflow cron expressions.

On This Page

Principle 1: Keep Pipelines FastPrinciple 2: Fail FastPrinciple 3: Use Caching EffectivelyNode.js cachingPython cachingMaven cachingPrinciple 4: Use Artifacts SparinglyPrinciple 5: DRY Pipelines with YAML Anchors and AliasesPrinciple 6: Use `include` to Share Pipeline TemplatesPrinciple 7: Keep Secrets Out of the Pipeline DefinitionPrinciple 8: Use `needs` for DAG PipelinesPrinciple 9: Use `rules` Instead of Only `only/except`Principle 10: Protect Production with Manual Gates and Protected EnvironmentsCommon Mistakes to AvoidNo cachingUsing `latest` images without pinningToo many stagesArtifact sprawlCopy-paste YAML everywhereHidden secrets in logsMeasuring Pipeline Performance with GitLab CI AnalyticsRefactored Pipeline Example: Before vs AfterBeforeAfterFinal TakeawayKnowledge Check