GitLab CI Error Guide: 'prepare environment: exit status 1' Shell Profile Loading Failure
Fix GitLab Runner 'prepare environment: exit status 1' system failures: a broken ~/.bashrc, /etc/profile.d script, missing $HOME, or SELinux on shell executors.
- #gitlab-cicd
- #troubleshooting
- #errors
- #runners
Exact Error Message
The job fails almost instantly — before any of your script: runs — and the log ends with a system failure:
ERROR: Job failed (system failure): prepare environment: exit status 1. Check https://docs.gitlab.com/runner/shells/#shell-profile-loading for more information
A close variant points at the runner’s own user and shell:
Running with gitlab-runner 17.x
Preparing the "shell" executor
ERROR: Job failed (system failure): prepare environment: exit status 1. Check https://docs.gitlab.com/runner/shells/#shell-profile-loading for more information
The phrase “system failure” (as opposed to “script failure”) and the link to the shell-profile-loading docs are the signature: the runner could not even get a usable login shell, so it never reached your commands.
What the Error Means
On a shell executor (and the SSH executor), GitLab Runner does not run your script in a bare shell. It starts a login shell for the runner’s user — typically gitlab-runner — to set up the environment. A login shell sources the user’s profile chain: /etc/profile, /etc/profile.d/*.sh, then ~/.bash_profile/~/.profile and ~/.bashrc, and on logout ~/.bash_logout.
If any of those scripts exits non-zero — an explicit exit 1, a return 1 outside a function, a set -e followed by a failing command, a reference to an unset variable under set -u, or simply a syntax error — the login shell itself returns a non-zero status. GitLab Runner interprets that as “I could not prepare the environment” and aborts the job as a system failure before running a single line of your .gitlab-ci.yml. The job’s own script: is irrelevant here; the problem is the runner host’s shell startup files.
This only affects executors that start a login shell on the host (shell, SSH). The Docker and Kubernetes executors run inside a container and are generally immune.
Common Causes
- A profile script that exits non-zero.
exit 1, a strayreturn, or a command failing underset -ein~/.bashrc,~/.bash_profile,~/.profile, or/etc/profile.d/*.sh. - A broken
~/.bash_logout. The login shell also runs logout hooks; a failing~/.bash_logoutmakes the whole shell exit non-zero even though startup succeeded. - An interactive guard that misfires. Lines that
echo/reador that bail out when not interactive ([[ $- != *i* ]] && return) can return non-zero in the non-interactive CI shell. - Missing or wrong
$HOME. If thegitlab-runneruser has no home directory, or$HOMEis unset/owned by root, sourcing the profile fails. - The runner user’s login shell is
/sbin/nologinor/bin/false. The account was created as a service account with no real shell. - SELinux/AppArmor denial. A confined context blocks reading or executing the profile scripts;
dmesg/audit.logshows anAVC denied. - A recently added tool init line.
nvm,rbenv,conda, or a corporate/etc/profile.d/company.shthat errors when a dependency is absent.
How to Reproduce the Error
On a shell-executor host, add a failing line to the runner user’s profile and run any job:
# As root, break the gitlab-runner user's bashrc
echo 'exit 1' | sudo tee -a /home/gitlab-runner/.bashrc
# .gitlab-ci.yml — the script never runs
build:
tags: [shell]
script:
- echo "you will never see this"
The job fails with prepare environment: exit status 1 even though the script: is trivially correct, because the login shell aborted while sourcing .bashrc.
Diagnostic Commands
The fastest test is to become the runner user with a login shell and watch what the profile does — exactly what the runner does:
# Reproduce the runner's login shell as the runner user
sudo su - gitlab-runner -c 'echo OK; echo exit_status=$?'
# Trace which profile line fails
sudo su - gitlab-runner -c 'bash -lxc true' 2>&1 | tail -40
# Confirm the runner user's home and login shell are sane
getent passwd gitlab-runner # check shell is /bin/bash, not /sbin/nologin
sudo ls -la /home/gitlab-runner # $HOME exists and is owned by the user
# Inspect the profile chain
sudo cat /home/gitlab-runner/.bashrc /home/gitlab-runner/.bash_profile \
/home/gitlab-runner/.profile /home/gitlab-runner/.bash_logout 2>/dev/null
ls -la /etc/profile.d/
# Runner service logs around the failure
sudo journalctl -u gitlab-runner -n 100 --no-pager
# SELinux denials, if applicable
sudo ausearch -m avc -ts recent 2>/dev/null | tail -20
If su - gitlab-runner itself prints an error or returns non-zero, you have reproduced the bug outside GitLab entirely — fix it there and the job recovers. You can also raise verbosity on the runner with gitlab-runner --debug run (foreground) to see the prepare step, and set variables: { CI_DEBUG_TRACE: "true" } to expose more job-side detail once the shell is fixed.
Step-by-Step Resolution
-
Reproduce as the runner user with
sudo su - gitlab-runnerand read the error it prints. This is the same shell the runner uses. -
Trace the failing line with
bash -lxc trueand find which profile script exits non-zero. -
Neuter or fix the offending script. For a third-party
/etc/profile.d/*.shthat errors when a tool is missing, guard it instead of exiting:# /etc/profile.d/company.sh — guard, don't exit if command -v some_tool >/dev/null 2>&1; then eval "$(some_tool init -)" fi -
Make interactive-only blocks non-fatal. Replace a top-level
[[ $- != *i* ]] && return(which returns non-zero when sourced at the top of.bashrc) with a clean early-exit that returns success, or move interactive setup behind anifthat does not change the exit status. -
Fix
$HOMEand the login shell if needed:sudo usermod -s /bin/bash gitlab-runner sudo mkdir -p /home/gitlab-runner && sudo chown gitlab-runner:gitlab-runner /home/gitlab-runner -
Address SELinux by fixing the file context (
restorecon -v /home/gitlab-runner/.bashrc) rather than disabling enforcement. -
If profile loading is genuinely not needed, switch the runner to the Docker executor in
config.toml, which sidesteps host profiles entirely:[[runners]] executor = "docker" [runners.docker] image = "alpine:3.20" -
Restart with
sudo gitlab-runner restartand retry the job.
Prevention and Best Practices
- Keep the runner user’s profile minimal. A CI service account does not need
nvm, prompt themes, or interactive aliases. The smaller the profile, the less can break. - Never
exitfrom a sourced profile script. Usereturninside guards and make sure the last statement leaves a zero exit status. - Guard every tool initializer with
command -v tool >/dev/null && ...so a missing dependency degrades gracefully instead of failing the shell. - Pin and review
/etc/profile.d/changes — system-wide profile drops affect every job on the host and are easy to forget. - Prefer the Docker or Kubernetes executor for fleets, so jobs are isolated from host profiles entirely and “prepare environment” failures cannot occur.
- For quick triage, the free incident assistant can read a
prepare environmentlog and point at the likely profile script. More patterns live in the GitLab CI/CD guides.
Related Errors
- GitLab CI Error: stuck runners tag mismatch — job never reaches a runner at all.
- GitLab CI Error: Kubernetes pod timed out — the Kubernetes-executor equivalent of a failed prepare step.
- GitLab CI Error: Cannot connect to the Docker daemon (dind) — a later-stage failure inside a running job.
Frequently Asked Questions
Why does the job fail before my script: even runs? Because “prepare environment” happens first. On a shell executor the runner opens a login shell for its user and sources the profile chain. If that shell exits non-zero, the runner aborts as a system failure and never reaches your commands.
Which files are in the profile chain? For bash login shells: /etc/profile, /etc/profile.d/*.sh, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile, plus ~/.bashrc if sourced by those, and ~/.bash_logout on exit. Any one of them exiting non-zero triggers the error.
How do I reproduce it without GitLab? Run sudo su - gitlab-runner (a login shell as the runner user). If that errors or returns non-zero, you have the same failure the runner hits. Trace it with bash -lxc true.
Does this affect the Docker or Kubernetes executor? Generally no. Those run inside a container, not a login shell on the host, so host profile scripts do not apply. Switching executors is a valid workaround when host profiles are unmanageable.
Is it safe to just delete the runner user’s .bashrc? Often yes for a dedicated CI account, but first identify what broke — a /etc/profile.d/ script affects all users on the host, and deleting the wrong file can hide a deeper configuration problem.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.