Your tests pass on your laptop. The same commit then dies on a plain Ubuntu box with error while loading shared libraries: GLIBC_2.34 not found.
Was the code wrong? Usually not. The machine was different, and nobody wrote down how.
That gap gets wider now that agents edit code for us. An agent can bump a dependency in ten seconds. It cannot see that your laptop ships a newer libc than the box that runs the job.
So I stopped asking "did it run?" I ask "on what?". Here are four myths I had to drop first.
Myth 1: "Same image tag means the same runtime"
A tag is a moving pointer. The digest is the fact.
Check the digest, not the tag:
docker image inspect --format '{{index .RepoDigests 0}}' api:1.4
Two machines can pull the same tag and get two different builds. Tags get overwritten. Caches go stale. Mirrors lag.
Corrected model: a container is a filesystem plus a kernel contract. The kernel comes from the host. The libc comes from the image. Both matter.
Myth 2: "python --version is the version"
It is one number. Your dependency resolution depends on five.
- The patch level:
3.11.4and3.11.9differ in stdlib and security fixes. - The libc build: manylinux wheels are compiled against a glibc floor.
- The OpenSSL build:
ssl.OPENSSL_VERSIONchanges TLS behaviour and cipher defaults. - The architecture:
arm64wheels are notx86_64wheels. - The build flags: a distribution build is not a pyenv build.
Ask Python itself, not the CLI banner:
python3 - <<'PY'
import platform, ssl, sys
print("version = ", sys.version.split()[0])
print("impl = ", platform.python_implementation())
print("libc = ", " ".join(platform.libc_ver()))
print("openssl = ", ssl.OPENSSL_VERSION)
print("machine = ", platform.machine())
PY
Corrected model: "Python 3.11" is a family, not a fingerprint. Pin the patch level, and rebuild wheels where the glibc floor differs.
Myth 3: "Locale and timezone are cosmetic"
They change results. Silently, and only on some machines.
Sorting is the easy one to demo:
printf 'Zoe\nanna\nAlvaro\n' > names.txt
LC_ALL=C sort names.txt
LC_ALL=en_US.UTF-8 sort names.txt
Different order. Same file. If your test asserts on output ordering, it will flip between hosts.
Dates are worse. TZ unset means "whatever the host thinks". A naive datetime.now() then lands in a different day. Anything that groups by day, week, or billing period can drift.
Corrected model: locale and timezone are inputs to your program. Treat them like config. Pin TZ=UTC and LC_ALL=C.UTF-8 in the harness, then assert them in the fingerprint.
Myth 4: "If the free box is slow, buy bigger hardware"
Sometimes true. Often it is contention, cold cache, or a network hop.
Check steal time before you check the price list:
top -bn1 | head -n 5 # look at %st and %wa
awk '/^cpu / {print "steal=" $9, "iowait=" $6}' /proc/stat
- High
%st: another tenant is using the CPU. Bigger hardware does not fix a noisy neighbour. - High
%wa: it is disk, not CPU. - First run slow, later runs fast: cold cache, not capacity.
- Local fast, remote slow: network round trips, not compute.
Run the same workload three times. If the spread is huge, you have contention. Capacity planning can wait.
Corrected model: measure variance before you measure averages. Then decide if you need a bigger box or a quieter one.
The artifact: a 30-line fingerprint script
This prints facts, one per line. No dependencies beyond bash and coreutils.
#!/usr/bin/env bash
# fingerprint.sh - capture the runtime facts that break "works on my machine"
set -eu
echo "captured_at_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "uname=$(uname -srm)"
echo "arch=$(uname -m)"
echo "libc=$(getconf GNU_LIBC_VERSION 2>/dev/null || ldd --version 2>&1 | head -n1)"
echo "cpu_count=$(getconf _NPROCESSORS_ONLN)"
echo "mem_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo unknown)"
echo "tz=${TZ:-<unset>}"
echo "tz_offset=$(date +%z)"
echo "locale=$(locale 2>/dev/null | awk -F= '/^LC_ALL=|^LANG=/ {printf "%s;", $2}')"
for bin in python3 node go java; do
command -v "$bin" >/dev/null 2>&1 || continue
echo "$bin=$("$bin" --version 2>&1 | head -n1)"
done
Two notes. It reads /proc, so it is Linux-first. And set -eu will not kill the run when getconf is missing, thanks to the fallback.
Run it on both machines, then diff
Local first:
bash fingerprint.sh > laptop.txt
Then the second box, without copying anything:
ssh box 'bash -s' < fingerprint.sh > remote.txt
diff -u laptop.txt remote.txt
That ssh ... 'bash -s' < file trick pipes the script over stdin. No install step, no leftover file.
The diff is the artifact. Keep it next to the failing build.
Reading the diff: a small decision table
| Diff you see | Likely cause | Next step |
|---|---|---|
libc differs |
wheel vs glibc floor | rebuild wheels on the older box |
arch differs |
arm64 vs x86_64
|
use multi-arch images, or build per arch |
python3 patch differs |
stdlib or SSL drift | pin the patch level in your image |
tz or locale differs |
time and sort drift | pin both in the test harness |
only captured_at_utc differs |
environment looks matched | check data, credentials, network next |
If nothing differs and the bug is still there, good news. You just removed a whole category of suspects in two minutes.
Where a free tier actually fits
For this workflow I want a throwaway second machine. Not a fleet. One box I can fingerprint, break, and delete.
MonkeyCode's free server option is useful exactly there: a disposable second environment to diff against your laptop. The free model access helps too. I use it to turn a raw diff into a short "what changed, and why it matters" note for the issue.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Two honest caveats. Limits and capacity change, so check the project's current offer before you plan around it. And a free box is not a production target. Do not put secrets or customer data on it.
Limitations, and who should skip this
This approach is not for everyone.
-
Skip it if you already run hermetic builds. Pinned images, lockfiles, and
--platformflags already cover most of it. - Skip it in regulated environments. If code cannot leave your network, use an internal second host instead of any hosted box.
- Skip it for pure logic bugs. If the failure reproduces with the same inputs on one machine, drift is not your suspect.
- Do not treat a matching fingerprint as proof. It is a filter, not a guarantee.
One question before the next "it works"
Before you ship, ask: which machine proved it? If you cannot answer with a file, you have a vibe.
Run the script twice this week. Keep both outputs. Then ask whether a free second box earned a permanent slot in your loop.
Top comments (0)