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

Variables, Secrets and Environments in GitLab CI

PreviousPrev
Next

Master GitLab CI/CD variables, predefined variables, custom variables, masked and protected secrets, group-level variables, environment-scoped variables, and .env file injection.

As soon as a pipeline becomes useful, it needs data. A build may need the current branch name. A deploy job may need a cloud token. A Docker publish step may need a registry URL. A staging deployment may need one database connection string, while production needs another. This is where GitLab CI/CD variables become essential.

This tutorial explains GitLab CI variables, secrets, and environments in depth. You will learn what variables are, why they matter, which predefined variables are most useful, how to define custom variables in different places, how precedence works when values collide, how masked and protected variables improve security, how environment-scoped variables solve staging-versus-production differences, how group-level variables help teams reuse configuration across projects, how to reference variables in Linux and Windows scripts, how to pass values between jobs with a dotenv artifact, and how to handle multi-line credentials with file-type variables.

What Are CI/CD Variables and Why Do They Matter?

A CI/CD variable is a named value available to pipeline jobs at runtime.

Instead of hard-coding values into your repository, you inject them when the pipeline runs. That makes pipelines safer, more flexible, and easier to reuse.

Common things variables store

Variables can represent:

  • environment names such as staging or production
  • URLs such as API endpoints or database connections
  • version identifiers such as commit SHA values
  • credentials such as deploy tokens and API keys
  • feature flags or build-time settings
  • generated values passed from one job to another

Why hard-coding is a bad habit

If you place secrets directly in .gitlab-ci.yml or source code, you create obvious risks:

  • secrets become visible in Git history
  • rotating credentials becomes painful
  • the same repository cannot easily support multiple environments
  • teammates may accidentally leak tokens in forks or logs

Variables let you keep sensitive or changing values outside the committed application logic.

Predefined Variables in GitLab CI

GitLab automatically provides many predefined variables during pipeline execution. These variables describe the project, commit, branch, pipeline, environment, and registry context.

You do not define them yourself. GitLab injects them when jobs run.

Here are some of the most useful predefined variables for beginners.

CI_COMMIT_BRANCH

CI_COMMIT_BRANCH contains the branch name that triggered the pipeline.

Use it when you want branch-aware behavior, such as:

  • deploying only from main
  • naming preview environments from feature branches
  • adding branch names to logs or build metadata

Example:

script:
  - echo "Running on branch $CI_COMMIT_BRANCH"

CI_COMMIT_SHA

CI_COMMIT_SHA contains the full commit SHA for the pipeline's revision.

This is incredibly useful for:

  • tagging Docker images immutably
  • producing traceable release artifacts
  • linking deployments back to exact source commits

Example:

script:
  - echo "Building commit $CI_COMMIT_SHA"

CI_PROJECT_NAME

CI_PROJECT_NAME gives you the project name. It is helpful for logs, dynamic file names, or reusable scripts that behave differently depending on the project context.

CI_PIPELINE_ID

CI_PIPELINE_ID is a unique numeric identifier for the pipeline.

This is useful when you want:

  • unique artifact labels
  • build metadata tied to a run
  • simple traceability across logs and systems

CI_REGISTRY

CI_REGISTRY points to the container registry associated with GitLab.

If you are building and pushing container images, this variable helps avoid hard-coding registry locations.

CI_ENVIRONMENT_NAME

CI_ENVIRONMENT_NAME becomes especially useful in deployment jobs that define an environment such as staging or production.

It lets scripts know which environment they are targeting.

Example:

deploy:
  stage: deploy
  environment:
    name: staging
  script:
    - echo "Deploying to $CI_ENVIRONMENT_NAME"

Why predefined variables are powerful

These built-in values make pipelines context-aware without requiring manual configuration for every run. They are the foundation for reusable automation.

Defining Custom Variables in .gitlab-ci.yml

Not every value comes from GitLab automatically. You will often define your own custom variables.

One place to do this is directly in .gitlab-ci.yml.

Global variables

You can define variables at the top level so many jobs can reuse them:

variables:
  APP_ENV: "staging"
  API_BASE_URL: "https://api.example.com"

These values are easy to use across jobs.

Job-level variables

You can also define variables only for a specific job:

run-tests:
  stage: test
  variables:
    NODE_ENV: "test"
  script:
    - echo "Running with $NODE_ENV"

This keeps configuration close to the job that needs it.

When YAML-defined variables are appropriate

Variables in .gitlab-ci.yml are fine for:

  • non-secret settings
  • defaults that apply to all environments
  • feature toggles for pipeline behavior
  • readable examples and templates

They are not the right place for sensitive secrets such as production passwords or private keys.

Defining Variables in Project Settings

For secret or environment-specific values, the GitLab UI is often a better place.

Inside a project, you can usually add variables under Settings -> CI/CD -> Variables.

This approach is useful because:

  • secrets stay out of the repository
  • values can be updated without changing code
  • masking and protection can be enabled
  • environment scopes can be applied

Examples of variables that often belong in project settings:

  • DATABASE_URL
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • DEPLOY_TOKEN
  • SLACK_WEBHOOK_URL

Group-Level Variables

If your organization has multiple related projects, repeating the same variable in every project quickly becomes tedious.

That is where group-level variables help.

A group-level variable is defined once at the GitLab group level and can be inherited by projects within that group.

Why group-level variables are useful

They are ideal for shared configuration such as:

  • a common package registry token
  • organization-wide API endpoints
  • standard cloud account IDs
  • deployment credentials used by several services

Benefits

  • less duplication
  • more consistent configuration
  • easier rotation of shared secrets
  • simpler onboarding for new projects

Of course, you should still use principle-of-least-privilege thinking. Shared values should only be shared when that scope truly makes sense.

Variable Precedence: Which Value Wins?

A very practical question is: What happens if the same variable name is defined in multiple places?

That is where variable precedence matters.

In plain language, the most specific or higher-priority definition wins over a broader default.

While exact precedence can involve several pipeline sources and policy layers, a beginner-friendly mental model is:

  1. job-level variables can override broader YAML defaults
  2. more specific pipeline-supplied values can override general ones
  3. project or group settings may override or be overridden depending on the source and configuration context

Why this matters in practice

Suppose you define:

  • a group variable API_URL=https://api.company.com
  • a project variable API_URL=https://api.special-service.com
  • a job variable API_URL=https://api.test.local

The job may see the most specific value intended for that context rather than the broadest one.

Best practice for sanity

To reduce confusion:

  • avoid duplicate variable names unless you intentionally want overrides
  • use clear naming like STAGING_DB_URL and PRODUCTION_DB_URL when appropriate
  • document your variable strategy for the team

Good naming is often easier to manage than clever precedence tricks.

Masked Variables

A masked variable helps prevent sensitive values from being exposed in job logs.

If a job outputs a masked value exactly, GitLab attempts to replace it with [MASKED] or an equivalent hidden representation in the logs.

Why masking matters

Logs are frequently read by many people. If a token accidentally appears in output, masking reduces the risk of an immediate secret leak.

Important masked variable requirements

For GitLab to mask a variable reliably, the value must satisfy certain requirements. A common beginner summary is:

  • it should not contain newlines
  • it should be at least 8 characters long
  • it must fit GitLab's masking rules and supported character patterns

If the value does not meet those rules, GitLab may reject masking or fail to mask the value safely.

Very important limitation

Masking is a safety net, not a license to print secrets. You should still avoid commands that echo secret values directly.

Bad example:

script:
  - echo "$DEPLOY_TOKEN"

Even if masking is enabled, printing secrets is poor practice. Some transformations or formatting can bypass exact masking behavior.

Protected Variables

A protected variable is only exposed to jobs that run on protected branches or protected tags.

This is especially important for production credentials.

Why protected variables matter

Imagine an untrusted feature branch pipeline had access to your production cloud secrets. That would be dangerous. Protected variables reduce that risk by limiting secret availability to trusted refs such as:

  • main
  • release/*
  • protected tags used for formal releases

Typical use cases

Protected variables are often used for:

  • production deploy tokens
  • production cloud keys
  • signing credentials
  • release publishing secrets

Masked + Protected: The Best Combination for Production Secrets

For highly sensitive values, the strongest beginner default is often to make them both masked and protected.

That combination means:

  • the value is hidden in logs where possible
  • the value is only available on trusted branches or tags

This is a strong baseline for secrets such as:

  • production database credentials
  • container registry deploy tokens
  • cloud provider keys used only for production release jobs

It is not the whole security story, but it is a much better start than plain text secrets in YAML.

Environment-Scoped Variables

A pipeline often deploys to more than one environment. Staging and production should not share the same secrets.

That is where environment-scoped variables shine.

The core idea

You can define the same variable name with different values depending on the environment scope.

For example:

  • DB_URL scoped to staging -> points to the staging database
  • DB_URL scoped to production -> points to the production database

Then your deploy job can reference the same variable name while GitLab injects the correct value for the target environment.

Why this is elegant

This lets you keep your pipeline logic clean. Instead of hard-coding different variable names throughout the scripts, you can write deployment logic once and let GitLab provide the appropriate environment-specific value.

Example mental model

A deployment job defines:

environment:
  name: production

Because the environment is production, GitLab can provide the DB_URL value scoped to production rather than staging.

Real-world value

Environment scopes are excellent for:

  • database URLs
  • API endpoints
  • feature flag service keys
  • cluster credentials
  • per-environment service tokens

Referencing Variables in Scripts

How you reference a variable depends on the shell environment.

Linux and Unix-style shells

In most GitLab Docker jobs, you use shell syntax like:

$VAR_NAME

or:

${VAR_NAME}

Example:

script:
  - echo "Project: $CI_PROJECT_NAME"
  - echo "Branch: $CI_COMMIT_BRANCH"

Windows shells

In Windows command environments, variable syntax often looks like:

%VAR_NAME%

That matters if your runner uses a Windows shell rather than a Linux-based container.

Why syntax awareness matters

A variable may exist correctly but still appear “broken” if you use the wrong shell syntax.

The Dotenv Artifact: Passing Variables Between Jobs

Sometimes a job computes a value during runtime and a later job needs it. This is where artifacts:reports:dotenv becomes very useful.

The problem it solves

Imagine a build job generates:

  • a dynamic version number
  • a release URL
  • an image tag
  • a short-lived derived variable

A later deploy job needs that value, but ordinary shell exports do not automatically carry across jobs because each job runs in its own isolated environment.

The GitLab solution

A job can write variables into a dotenv-style file, then publish that file as a report artifact.

Example:

build-version:
  stage: build
  script:
    - echo "APP_VERSION=1.2.$CI_PIPELINE_ID" > build.env
  artifacts:
    reports:
      dotenv: build.env

deploy:
  stage: deploy
  script:
    - echo "Deploying version $APP_VERSION"

When configured correctly, downstream jobs can receive APP_VERSION automatically.

Why this is better than hacks

This approach is cleaner than copy-pasting values into multiple jobs or trying to infer runtime data repeatedly.

File-Type Variables

Some secrets are not simple one-line strings. Examples include:

  • SSH private keys
  • kubeconfig files
  • JSON service account credentials
  • certificate bundles

For these cases, GitLab supports file-type variables.

How file-type variables work

Instead of exposing the raw content directly as a normal string value, GitLab makes the variable available as a file path pointing to a temporary file containing the secret content.

This is useful because many tools expect a file rather than a shell variable.

Why they matter

They help avoid awkward quoting problems and make multi-line credential handling much more practical.

Example use cases:

  • kubectl using a kubeconfig file
  • SSH commands using a private key file
  • cloud SDKs reading JSON credentials from disk

A Complete Example: Managing Database URLs, API Keys, and Deploy Tokens Properly

Let us bring the concepts together.

Imagine a project that has:

  • a test job that calls a public API
  • a staging deployment
  • a production deployment
  • a Docker image push step

A sensible setup might look like this:

In .gitlab-ci.yml

stages:
  - test
  - deploy

variables:
  APP_NAME: "shop-service"

test-api:
  stage: test
  image: node:20
  script:
    - echo "Testing $APP_NAME on branch $CI_COMMIT_BRANCH"
    - npm ci
    - npm test

deploy-staging:
  stage: deploy
  image: alpine:3.20
  environment:
    name: staging
  script:
    - echo "Deploying $APP_NAME to $CI_ENVIRONMENT_NAME"
    - ./deploy.sh "$DB_URL" "$API_KEY"

deploy-production:
  stage: deploy
  image: alpine:3.20
  environment:
    name: production
  script:
    - echo "Deploying $APP_NAME to $CI_ENVIRONMENT_NAME"
    - ./deploy.sh "$DB_URL" "$API_KEY"

In project or group settings

You might define:

  • API_KEY as masked
  • DEPLOY_TOKEN as masked and protected
  • DB_URL scoped separately for staging and production
  • a file-type variable for CLOUD_SERVICE_ACCOUNT_JSON

Why this setup is strong

  • the repository contains only non-secret defaults
  • staging and production use the same variable names but different scoped values
  • production secrets are protected from untrusted branches
  • sensitive values are not committed to Git
  • the deploy scripts stay reusable

This is exactly the kind of pattern that scales well.

Security Best Practices for GitLab CI Secrets

Security is not just about features. It is about habits.

Never echo secrets

Even masked secrets should not be printed deliberately.

Use masked variables for sensitive strings

Masking helps reduce accidental exposure in logs.

Use protected variables for production credentials

Production secrets should normally be limited to protected refs.

Rotate secrets regularly

A secret that never changes becomes a long-term liability. Build a rotation habit.

Prefer least privilege

A token should only be able to do what the job actually needs.

Avoid storing secrets in the repository

Even private repositories are not a good excuse for committing secrets.

Use file-type variables for structured multi-line credentials

They are easier to manage safely than trying to inline complex content.

Review who can trigger protected pipelines

Secret protection is only as good as the branch and permission model around it.

Common Beginner Mistakes with Variables

A few mistakes appear frequently when people first learn GitLab CI secrets management.

Mistake 1: Using YAML variables for secrets

A value in .gitlab-ci.yml is visible in the repository. Do not put production secrets there.

Mistake 2: Forgetting environment scopes

If staging and production share the same DB_URL, you might accidentally deploy production code against the wrong database.

Mistake 3: Assuming masking makes logging safe

Masking helps, but you should still avoid printing secret values.

Mistake 4: Reusing one powerful token everywhere

Different jobs and environments should not always share the same broad credential.

Mistake 5: Ignoring precedence conflicts

If the same variable exists in multiple places, debugging can become confusing unless naming and ownership are clear.

Final Takeaway

Variables are the configuration language of real-world pipelines. They let you inject context, protect secrets, reuse logic across environments, and keep sensitive data out of version control.

In GitLab CI/CD, the most important things to remember are:

  • predefined variables provide rich pipeline context
  • custom variables can be defined in YAML, project settings, or group settings
  • masked variables help hide secrets in logs
  • protected variables restrict access to trusted branches and tags
  • environment-scoped variables let one pipeline safely target multiple environments
  • dotenv artifacts pass generated values from one job to another
  • file-type variables are ideal for multi-line credentials

If you design variable management well early on, your pipelines become much safer and much easier to scale.

Knowledge Check

Exercise

Question 1: Predefined Variables

Which predefined GitLab CI variable is most useful when you want to know the branch that triggered the pipeline?

Exercise

Question 2: Secret Protection

What is the main benefit of marking a variable as both masked and protected?

Exercise

Question 3: Environment Scope

Why are environment-scoped variables useful in GitLab CI/CD?

PreviousPrev
Next

Continue Learning

GitLab CI Stages and Jobs

Master GitLab CI/CD stages and jobs, learn how to define pipeline stages, write job scripts, use rules and only/except conditions, control job ordering, and run parallel jobs.

12 min read·Easy

GitLab CI Artifacts

Learn GitLab CI artifacts, how to define, use, and download build artifacts, pass files between jobs, set expiration times, and use artifacts for test reports and deployment packages.

10 min read·Easy

GitLab CI Cache

Speed up your GitLab CI pipelines with caching, learn how to define cache paths, set cache keys, understand cache policies, and cache node_modules, pip packages, and Maven dependencies.

10 min read·Easy

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

What Are CI/CD Variables and Why Do They Matter?Common things variables storeWhy hard-coding is a bad habitPredefined Variables in GitLab CI`CI_COMMIT_BRANCH``CI_COMMIT_SHA``CI_PROJECT_NAME``CI_PIPELINE_ID``CI_REGISTRY``CI_ENVIRONMENT_NAME`Why predefined variables are powerfulDefining Custom Variables in `.gitlab-ci.yml`Global variablesJob-level variablesWhen YAML-defined variables are appropriateDefining Variables in Project SettingsGroup-Level VariablesWhy group-level variables are usefulBenefitsVariable Precedence: Which Value Wins?Why this matters in practiceBest practice for sanityMasked VariablesWhy masking mattersImportant masked variable requirementsVery important limitationProtected VariablesWhy protected variables matterTypical use casesMasked + Protected: The Best Combination for Production SecretsEnvironment-Scoped VariablesThe core ideaWhy this is elegantExample mental modelReal-world valueReferencing Variables in ScriptsLinux and Unix-style shellsWindows shellsWhy syntax awareness mattersThe Dotenv Artifact: Passing Variables Between JobsThe problem it solvesThe GitLab solutionWhy this is better than hacksFile-Type VariablesHow file-type variables workWhy they matterA Complete Example: Managing Database URLs, API Keys, and Deploy Tokens ProperlyIn `.gitlab-ci.yml`In project or group settingsWhy this setup is strongSecurity Best Practices for GitLab CI SecretsNever echo secretsUse masked variables for sensitive stringsUse protected variables for production credentialsRotate secrets regularlyPrefer least privilegeAvoid storing secrets in the repositoryUse file-type variables for structured multi-line credentialsReview who can trigger protected pipelinesCommon Beginner Mistakes with VariablesMistake 1: Using YAML variables for secretsMistake 2: Forgetting environment scopesMistake 3: Assuming masking makes logging safeMistake 4: Reusing one powerful token everywhereMistake 5: Ignoring precedence conflictsFinal TakeawayKnowledge Check