GitLab CI Error Guide: './script.sh: No such file or directory' Command Not Found
Fix GitLab's 'No such file or directory' and 'command not found': chmod +x, wrong paths, CRLF line endings, missing shebangs, and tools not in the image.
- #gitlab-cicd
- #troubleshooting
- #errors
- #scripts
Exact Error Message
The job log shows one of these just before it fails:
$ ./deploy.sh
/bin/bash: line 123: ./deploy.sh: No such file or directory
ERROR: Job failed: exit code 127
Or, when the command itself is missing from PATH:
$ terraform plan
/bin/sh: 1: terraform: not found
ERROR: Job failed: exit code 127
$ jq '.version' package.json
bash: jq: command not found
ERROR: Job failed: exit code 127
The tell-tale sign is exit code 127 — the shell’s dedicated code for “command not found” — as opposed to the generic exit code 1 that means a command ran but returned an error.
What the Error Means
There are two distinct flavours hiding under similar wording:
./script.sh: No such file or directory— the shell found something to run but couldn’t actually execute it. Either the file isn’t there, isn’t executable, or its#!interpreter line points at a binary that doesn’t exist (classic CRLF symptom:#!/bin/bash\rlooks for an interpreter literally namedbash\r).<cmd>: not found/command not found— the shell searched every directory inPATHand never found a binary by that name. The tool simply isn’t installed in the container image, or it lives somewhere not onPATH.
Both end in exit code 127, which is how you distinguish this class of failure from a command that ran and merely failed (exit 1).
Common Causes
- Script isn’t executable. Git tracked the file without the
+xbit, so./deploy.shcan’t be run directly. - Wrong path or working directory. The script lives in
scripts/deploy.shbut the job runs./deploy.shfrom the repo root. - The file isn’t in the job at all. It was produced by a previous job and never passed forward via
artifacts:/needs:, orGIT_STRATEGY: noneskipped the checkout. - CRLF line endings. A file edited on Windows has
\r\n; the shebang becomes#!/bin/bash\r, and the kernel looks for an interpreter that doesn’t exist. - Missing shebang. Running
./script.shwith no#!line means the kernel can’t pick an interpreter. - Tool not installed in the image.
terraform,jq,aws,kubectl, etc. aren’t in the base image you chose. shvsbash. Many slim images default tosh(dash/busybox), which lacks bash-isms ([[ ]], arrays) and may not even include bash.
How to Reproduce the Error
A non-executable script reproduces flavour 1 instantly:
deploy:
image: alpine:3.20
script:
- ./deploy.sh # committed without chmod +x
/bin/sh: ./deploy.sh: not found
A missing tool reproduces flavour 2:
plan:
image: alpine:3.20 # no terraform, no jq, no bash
script:
- jq --version
/bin/sh: jq: not found
Diagnostic Commands
Add a debug block to the start of the failing job so the log shows the ground truth before the failing command:
script:
- pwd && ls -la # where am I, what files exist here?
- ls -la scripts/ # is the script in the path I expect?
- file ./deploy.sh # is it a script or something else?
- head -1 ./deploy.sh # what does the shebang say?
- cat -A ./deploy.sh | head # ^ shows $ for LF, ^M$ for CRLF
- which jq || echo "jq not on PATH"
- echo "$PATH"
- ./deploy.sh
What each line tells you:
pwd && ls -la— confirms the working directory and whether the file is present and executable (look for thexbits in-rwxr-xr-x).file ./deploy.sh— reports the file type and, for scripts, the interpreter.head -1 ./deploy.sh— shows the shebang so you can spot a missing or wrong one.cat -A ./deploy.sh— exposes hidden characters: a line ending in^M$is CRLF, the most insidious cause of a “valid” script that “doesn’t exist.”which <cmd>andecho "$PATH"— confirm whether a tool is installed and reachable.
For a deeper trace, set CI_DEBUG_TRACE: "true" to see the full generated shell wrapper and exactly which line the runner reached.
Step-by-Step Resolution
1. Make the script executable and commit the mode bit:
chmod +x deploy.sh
git update-index --chmod=+x deploy.sh # ensure the +x is recorded in git
git commit -am "make deploy.sh executable"
Alternatively, sidestep the executable bit by invoking the interpreter explicitly:
script:
- bash scripts/deploy.sh # runs even without +x or a shebang
2. Fix the path / working directory. Use the real path, or cd first:
script:
- cd scripts && ./deploy.sh
# or
- ./scripts/deploy.sh
3. Strip CRLF line endings if cat -A showed ^M$:
before_script:
- apt-get update && apt-get install -y dos2unix
- dos2unix scripts/*.sh
Better, prevent it at the source with a .gitattributes entry so the repo always stores LF:
*.sh text eol=lf
4. Add or fix the shebang as the first line of the script:
#!/usr/bin/env bash
set -euo pipefail
5. Install the missing tool or pick the right image. Either install it in before_script:
before_script:
- apk add --no-cache jq curl bash # alpine
# or: apt-get update && apt-get install -y jq curl # debian/ubuntu
…or, far cleaner, use an image that already ships the tool:
plan:
image: hashicorp/terraform:1.9 # terraform pre-installed
script:
- terraform plan
6. Use bash explicitly if you need bash-isms. On a slim image where sh is the default shell, either install bash and set the shell, or switch to an image with bash built in. If the file must run via bash, call bash script.sh rather than ./script.sh.
Prevention and Best Practices
- Commit the executable bit (
git update-index --chmod=+x) and verify withls -lain CI the first time. - Enforce LF endings with a
.gitattributesrule (*.sh text eol=lf) so Windows checkouts never corrupt shebangs. - Always start scripts with
#!/usr/bin/env bashandset -euo pipefailfor predictable behaviour. - Choose an image that already contains your tools instead of installing them on every run — it’s faster and removes a class of “not found” failures.
- Pin image tags so a tool that exists today isn’t dropped when
latestrebuilds. - Reference scripts from the repo root or
cddeliberately; don’t assume the working directory.
Related Errors
- GitLab CI Error Guide: ‘ERROR: Job failed: exit code 1’ — when the command ran but returned non-zero rather than being missing (exit 1 vs exit 127).
- GitLab CI Error Guide: shallow clone ‘reference is not a tree’ — when a needed file is missing because of how the repo was checked out.
- GitLab CI Error Guide: ‘Invalid CI config’ — config-level failures before any script runs.
Frequently Asked Questions
What’s the difference between No such file or directory and command not found?
No such file or directory (for a path like ./deploy.sh) usually means the file is missing, not executable, or has a broken shebang — the shell found a path but couldn’t run it. command not found means the shell searched PATH and the binary isn’t installed anywhere. Both produce exit code 127.
My script exists and I can see it with ls, but I still get No such file or directory. Why?
Almost always CRLF line endings. The shebang #!/bin/bash becomes #!/bin/bash\r, so the kernel looks for an interpreter literally named bash\r and fails. Run cat -A script.sh — a trailing ^M$ confirms it. Fix with dos2unix and a .gitattributes eol=lf rule.
Why does my bash script fail on an Alpine image?
Alpine’s default shell is busybox sh, and bash may not be installed at all. Either apk add bash and call bash script.sh, or use a Debian/Ubuntu-based image. Bash-only syntax like [[ ]] or arrays will break under sh.
A tool works locally but is not found in CI. How do I fix it?
Your local machine has the tool installed; the CI container doesn’t. Install it in before_script (apk add / apt-get install) or, better, choose an image: that already includes it (e.g. hashicorp/terraform, amazon/aws-cli). Run which <tool> in the job to confirm.
Do I have to chmod +x my scripts?
Only if you invoke them directly as ./script.sh. If you call the interpreter explicitly — bash script.sh or python script.py — the executable bit doesn’t matter. For direct invocation, commit the +x bit with git update-index --chmod=+x.
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.