1. Introduction

Environment variables that don't work in CI/CD pipelines are responsible for a surprising share of build failures. The symptom can be almost anything: authentication errors, "variable not found" messages, commands connecting to the wrong environment, or deployments going to the wrong cluster. The cause is always some form of variable not being available where it's needed, or having the wrong value.

This guide covers GitLab CI, GitHub Actions, and Jenkins, with platform-specific patterns for the most common env var problems.

2. How CI/CD Environment Variables Work

Every CI/CD platform has a concept of variable scope — the set of jobs, stages, or pipelines that can access a variable. When a variable isn't working, it's almost always a scope problem: the variable exists somewhere in the system, but not in the context where the failing job runs.

3. Common Causes

4. Step-by-Step Fix

Step 1: Confirm which variables are available in the failing job

# GitLab CI — add a debug job to print all non-sensitive variables
debug-vars:
  stage: .pre
  script:
    - env | grep -v 'SECRET\|TOKEN\|PASSWORD\|KEY' | sort
    - echo "CI_REGISTRY=$CI_REGISTRY"
    # Check a specific variable exists (without printing value):
    - test -n "$MY_VARIABLE" && echo "MY_VARIABLE is set" || echo "MY_VARIABLE is MISSING"
  when: manual

# GitHub Actions:
- name: Debug env
  run: |
    env | grep -v 'TOKEN\|SECRET\|KEY' | sort
    echo "MY_VAR set: ${{ env.MY_VAR != '' }}"

# Jenkins:
stage('Debug') {
    steps {
        sh 'env | grep -v "TOKEN\|SECRET\|PASSWORD" | sort'
        sh 'echo "DEPLOY_ENV=${DEPLOY_ENV}"'
    }
}

Step 2: Fix variable scope issues in GitLab CI

"# Variable only available in a specific environment:
# Check: Settings → CI/CD → Variables → see "Environment scope" column
# Change scope from "production" to "*" if needed for all environments

# Variable is "Protected" but branch isn't:
# Either: remove "Protected" flag from the variable
# Or:    protect the branch (Settings → Repository → Protected branches)

# Pass a variable explicitly to a specific job:
my-job:
  variables:
    MY_VAR: "value"  # job-level variable, overrides project-level
  script:
    - echo "$MY_VAR"

# Reference a parent group variable:
# Variables at group level are available to all projects in the group
# Check: Group → Settings → CI/CD → Variables

Step 3: Fix variables not available in Docker containers

# When you run docker inside your CI job, the container doesn't
# automatically inherit the CI job's environment variables.

# Wrong — $MY_SECRET is not available inside the container:
- docker run my-image ./deploy.sh

# Correct — explicitly pass variables:
- docker run -e MY_SECRET="$MY_SECRET" -e DEPLOY_ENV="$DEPLOY_ENV" my-image ./deploy.sh

# Pass all variables matching a pattern:
- docker run $(env | grep '^DEPLOY_\|^AWS_' | sed 's/^/-e /') my-image ./deploy.sh

# Or use --env-file for many variables:
- env | grep '^APP_' > /tmp/.env && docker run --env-file /tmp/.env my-image ./deploy.sh

Step 4: Fix shell expansion issues

# Double quotes vs single quotes matter enormously for variable expansion

# Wrong: single quotes prevent variable expansion
echo 'The value is $MY_VAR'   # prints literally: The value is $MY_VAR

# Correct: double quotes expand variables
echo "The value is $MY_VAR"   # prints: The value is actual-value

# Variable with special characters (spaces, quotes) needs quoting:
MY_VAR="hello world"
command "$MY_VAR"    # correct: passes as single argument
command $MY_VAR      # wrong: passes as two arguments "hello" and "world"

# Variables containing special shell characters (&, |, ;, *, etc)
# should be wrapped in single quotes when assigned:
MY_VAR='value&with&special&chars'

# Check for hidden special characters in variable values:
echo "$MY_VAR" | od -c | head  # shows all characters including invisible ones

Step 5: Fix GitHub Actions variable scoping

"# Secrets must be explicitly exposed as env vars in each step
# Wrong:
- name: Deploy
  run: ./deploy.sh  # $MY_SECRET is not available here

# Correct:
- name: Deploy
  env:
    MY_SECRET: ${{ secrets.MY_SECRET }}
    DEPLOY_ENV: ${{ secrets.DEPLOY_ENV }}
  run: ./deploy.sh

# To share a computed variable between steps in the same job:
- name: Set version
  id: version
  run: echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT

- name: Use version
  run: echo "Deploying ${{ steps.version.outputs.VERSION }}"

# To share between jobs, use job outputs:
jobs:
  build:
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - id: version
        run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
  deploy:
    needs: build
    steps:
      - run: echo "Deploying ${{ needs.build.outputs.version }}"

Step 6: Fix Jenkins variable scope across stages

"// In Jenkins Declarative Pipeline, variables defined in one stage
// are NOT available in other stages by default

// Wrong:
stage('Build') {
    steps {
        script {
            def version = sh(script: 'cat VERSION', returnStdout: true).trim()
        }
    }
}
stage('Deploy') {
    steps {
        sh "deploy.sh $version"  // version not available here!
    }
}

// Correct: use env to share across stages
stage('Build') {
    steps {
        script {
            env.APP_VERSION = sh(script: 'cat VERSION', returnStdout: true).trim()
        }
    }
}
stage('Deploy') {
    steps {
        sh "deploy.sh ${env.APP_VERSION}"  // now works
    }
}

5. Verification Steps

# Add an explicit check at the start of jobs that need specific variables:
script:
  - |
    REQUIRED_VARS="DEPLOY_TOKEN KUBE_NAMESPACE AWS_REGION"
    for var in $REQUIRED_VARS; do
      if [ -z "${!var}" ]; then
        echo "ERROR: Required variable '$var' is not set"
        exit 1
      fi
    done
    echo "All required variables are set"
  - ./your-actual-deploy-command

6. Common Mistakes

7. Prevention Tips

8. FAQ

A variable is set in GitLab project settings but shows as empty in the job. Why?

Check three things: (1) Is the variable "Protected" and is the branch protected? Protected variables only work on protected branches. (2) Is the variable scoped to a specific environment? It won't be available in jobs that don't match that environment name. (3) Did a job-level variables: block accidentally override it with an empty value?

How do I pass a multi-line variable (like a private key) to CI?

In GitLab, create a "File" type variable — GitLab writes the value to a temp file and sets the env var to the file path. Access it with cat $MY_KEY_FILE or use the path directly for tools like ssh-agent. In GitHub Actions, store the key in a secret and write it to a file in a step: echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa.

9. Summary

SymptomCauseFix
Variable empty on feature branchGitLab "Protected" variableRemove protected flag or protect the branch
Variable missing in Docker containerDocker doesn't inherit CI envPass with -e MY_VAR="$MY_VAR" in docker run
Variable works in build, not deployScope doesn't cross stages (Jenkins)Use env.MY_VAR = value in script block
Single-quoted variable not expandingShell quoting errorChange to double quotes for variable expansion
Variable with special chars causes errorsUnquoted variable in shellAlways quote: "$MY_VAR" not $MY_VAR

Explore More in This Category

Explore more in this category: CI/CD guides. Browse all DevOps Compass articles or jump to: Kubernetes, AWS, CI/CD, Containers, Monitoring, Networking.