Site icon New Generation Enterprise Linux

Why your CI/CD build fails on the runner, but not on your laptop

DevOps Tooling On Linux (CI/CD Runners, Ansible)

Why your CI/CD build fails on the runner, but not on your laptop

Technical Briefing | 10/2/2026

You’ve written the code, it runs perfectly on your dev machine, all tests pass. You push it, the CI/CD pipeline kicks off, and then… red. ‘Command not found,’ or ‘Permission denied,’ or worse, some cryptic error from a scripting language that makes no sense. We’ve all been there. It’s the classic “works on my machine” nightmare, but with a twist: the machine *is* a Linux server, just like yours. The difference? Its environment often isn’t what you’re expecting. Most times, it’s not the code itself, but the subtle, insidious differences in environment variables that trip things up.

Your Runner’s PATH Is Lying To You (Probably)

The `PATH` variable is where most of these headaches start. On your dev machine, logging in usually sources a bunch of dotfiles (`.bashrc`, `.profile`, `.zshrc`) that set up a comfy, expansive `PATH`. Your CI runner, though? It’s often running a non-interactive, non-login shell. This means it might completely skip those files, or only source a very minimal set, leading to a much leaner `PATH` than you’re used to. Suddenly, your go-to command that lives in `/usr/local/bin` isn’t found, because that directory isn’t in the runner’s `PATH`. I’ve seen this silently fail countless times when teams move from manual deployments to CI. You’ll rely on some tool installed by a third-party script, assuming it’s globally available. But in a clean runner environment, it simply isn’t there, or it’s in a non-standard location that your minimalist shell doesn’t pick up.

The umask That Bit Me In Prod

Permissions, right? Always permissions. But it’s not always `chmod` or `chown`. Sometimes, it’s `umask`, which is often overlooked. `umask` defines the *default* permissions for newly created files and directories. A common `umask` is `0022`, which means files get `0644` (rw-r–r–) and directories get `0755` (rwxr-xr-x). Great. But what if your runner is using `0002`? Now files are `0664` and directories are `0775`. Or worse, what if it’s `0077`? That makes things private by default, `0600` and `0700` respectively. If a subsequent build step or a different user/service needs to read or write to those newly created artifacts, boom, ‘Permission denied,’ and you’re debugging permissions that *look* right, but aren’t. This bit me in prod when a CI job was generating configuration files for an application. The build passed, but the application couldn’t read its own config. Turns out, the `umask` in the runner was much stricter than the server where the app ran, making the generated files inaccessible to the app’s user.

umask
umask 0022
echo "Some content" > /tmp/testfile.txt
ls -l /tmp/testfile.txt

Locales: Don’t Let Them Ghost Your Builds

Here’s another subtle one: `locale` variables like `LANG`, `LC_ALL`, `LC_CTYPE`. These dictate how your system handles character sets, sorting order, date formats, and more. Most minimalist container images or barebone VMs used for CI/CD runners often have `locale` settings that are either undefined or set to a very basic `C` or `POSIX` locale. This can wreak havoc on scripts that expect UTF-8, especially when dealing with text processing, string comparisons, or even just `sort` operations. I’ve seen Python scripts blow up with `UnicodeEncodeError` because the `locale` expected ASCII, or `grep` fail to find patterns with special characters. And `sort`? Forget about it. Its behavior changes wildly depending on the `LC_COLLATE` setting.

  • Explicitly set `LANG` and `LC_ALL` in your runner’s environment, e.g., `export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8`.
  • Ensure the necessary locale packages are installed on your runner image. Often, `locale-gen en_US.UTF-8` followed by `update-locale LANG=en_US.UTF-8` is needed if you’re customizing an image.
  • Be aware that tools like `sort` or `grep` can behave differently with `C` or `POSIX` locales versus UTF-8. Test your scripts in an environment matching your target.
  • Python scripts are particularly sensitive to `locale` for file I/O and string operations; check for `UnicodeEncodeError` or `UnicodeDecodeError`.

The takeaway here is simple: your CI/CD runner is not your laptop. It’s a clean slate, and while that’s great for consistency, it means you can’t assume much about its environment. When a build inexplicably fails on the runner, start by dumping its environment. A simple `env` command early in your pipeline can be incredibly revealing. Look for `PATH`, `umask`, `LANG`, `LC_ALL`, and anything else you rely on. Debugging these environmental differences will save you from pulling your hair out over issues that *should* work, but stubbornly refuse to.

Linux Admin Automation  |  © www.ngelinux.com  |  10/2/2026
0 0 votes
Article Rating
Exit mobile version