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
- Variable defined at the wrong scope (group vs project vs job)
- Variable not passed to Docker containers started within the pipeline
- GitLab: variable is "protected" but the branch isn't protected
- GitHub Actions: secret not exposed as env var in the step that needs it
- Jenkins: variable defined in one stage but referenced in another without passing it
- Variable name typo —
API_KEYvsAPI_TOKENvsAPIKEY - Shell variable expansion issues — using single quotes instead of double quotes
- Variable contains special characters that break shell expansion
- Variable set in a subshell that doesn't export to the parent
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
- Marking a GitLab variable as "Protected" and then wondering why it's not available on feature branches
- Assuming Docker containers inside CI inherit the CI environment — they don't unless you explicitly pass variables
- Using single quotes in shell when you meant double quotes — single quotes prevent variable expansion
- Naming conflicts — a job-level variable silently overrides a project-level variable with the same name
- Setting a variable in a subshell without exporting:
MY_VAR=value commandsets it only forcommand, not for subsequent commands - Putting secrets in plaintext in the Dockerfile or repository instead of using CI variable stores
7. Prevention Tips
- Document all required environment variables at the top of each job as a comment or in a README
- Add a validation step that checks all required variables are set before doing any real work
- Use a consistent naming convention for variables (e.g. all deploy-related vars start with
DEPLOY_) - Avoid storing sensitive values as environment variables in
.gitlab-ci.ymlorJenkinsfile— use the platform's secret store - For AWS credentials, use IRSA or IAM instance roles instead of storing
AWS_ACCESS_KEY_IDin CI variables — eliminates rotation overhead - Test your pipeline on a feature branch before merging — this catches scope issues with protected variables early
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
| Symptom | Cause | Fix |
|---|---|---|
| Variable empty on feature branch | GitLab "Protected" variable | Remove protected flag or protect the branch |
| Variable missing in Docker container | Docker doesn't inherit CI env | Pass with -e MY_VAR="$MY_VAR" in docker run |
| Variable works in build, not deploy | Scope doesn't cross stages (Jenkins) | Use env.MY_VAR = value in script block |
| Single-quoted variable not expanding | Shell quoting error | Change to double quotes for variable expansion |
| Variable with special chars causes errors | Unquoted variable in shell | Always 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.