My build passed on my laptop. It died on a free server in 40 seconds.
Same code. Same lockfile. Same commit hash. So what changed?
The answer is boring and useful: the environment changed. Not the app.
Below is the FAQ I wish I had read first. Each answer comes with a cheap check you can run today. The checks are read-only, so they are safe on a throwaway box.
Q1: "It builds locally, so it builds anywhere" — true?
No. That sentence hides three separate claims.
It hides architecture, C library, and compiler version. Each one can break a build alone.
The claim: my machine and the server are interchangeable.
What I actually see: wheels resolve differently, and native extensions rebuild from source. Sometimes the rebuild fails on a missing header.
Better model: your laptop is one sample. The server is a different sample. You need to diff them, not assume them.
Cheap check:
uname -srm
getconf GNU_LIBC_VERSION 2>/dev/null || echo "musl or unknown"
python3 -V
If uname -m differs, stop reading stack traces. Fix the environment question first.
Q2: "The environment is just the env vars" — is it?
Not even close. Env vars are the loudest signal and the least complete one.
You also inherit locale, timezone, file descriptor limits, CA bundle layout, and the writable paths. None of those live in your .env file.
Ask yourself this: does your code ever call datetime.now() with no timezone? Then TZ is part of your build.
Does it ever write to /tmp and read it back later? Then disk lifetime is part of your build too.
Q3: "Both boxes are on UTC, so time is time" — really?
Clock skew is real on cheap infrastructure. Wall clock also moves backwards during NTP correction.
So a duration measured with wall clock can be negative. A cache TTL can outlive its own start time.
I use two rules now:
- Measure durations with a monotonic clock.
- Store timestamps as UTC with an explicit offset.
And one check. Compare the server's clock to an HTTP Date header, not to your wristwatch:
curl -sI https://example.com | awk -F': ' 'tolower($1)=="date"{print "http.date=" $2}'
date -u +"local.utc=%Y-%m-%dT%H:%M:%SZ"
This is a eyeball comparison, not a measurement. Treat a visible gap of seconds as a warning sign, not a verdict.
Q4: "A free server's disk is a disk" — is it?
Usually it is a temporary directory wearing a disk costume.
So your cache warms up. Your next request lands on a fresh instance. The cache is gone, and you call it a race condition.
Three questions worth answering before you design around disk:
- Does the working directory survive a restart?
- Is
${TMPDIR:-/tmp}writable and how large does it look? - Does anything you write need to outlive a single process?
If the answer to the third one is yes, the disk is not your storage layer. Object storage or a database is.
Q5: "If curl works, the network is open" — no
Outbound HTTP on 443 says almost nothing. It says one port to one host worked once.
SMTP egress is often blocked. Custom ports are often blocked. DNS may resolve through a local resolver with different caching rules.
So test the ports you actually need. Not the ones you assume are fine.
The artifact: a read-only parity probe
Here is the script I run on both sides. It prints stable key=value lines so diff does the thinking.
#!/usr/bin/env bash
# parity_probe.sh - read-only environment facts, one key=value per line
set -u
emit() { printf '%s=%s\n' "$1" "$2"; }
emit os.kernel "$(uname -srm)"
emit os.arch "$(uname -m)"
emit os.libc "$(getconf GNU_LIBC_VERSION 2>/dev/null || echo unknown)"
emit cpu.count "$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo unknown)"
emit shell.tz "${TZ:-unset}"
emit date.utc "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
emit locale "$(locale 2>/dev/null | awk -F= '/^(LANG|LC_ALL)=/{print $2}' | paste -sd, -)"
emit ulimit.nofile "$(ulimit -n)"
emit tmp.writable "$( [ -w "${TMPDIR:-/tmp}" ] && echo yes || echo no )"
emit cwd.writable "$( [ -w . ] && echo yes || echo no )"
emit python.ver "$(python3 -V 2>&1 | tr -d '\n' || echo missing)"
emit ca.bundle "$( [ -f /etc/ssl/certs/ca-certificates.crt ] && echo debian-style || echo other-or-missing )"
emit env.keys "$(env | cut -d= -f1 | sort | paste -sd, -)"
# HTTP date header, for a rough clock comparison
emit http.date "$(curl -sI --max-time 5 https://example.com \
| awk -F': ' 'tolower($1)=="date"{print $2}' | tr -d '\r')"
# Outbound TCP reachability for the ports you actually use
probe_port() {
local host=$1 port=$2
if timeout 3 bash -c "exec 3<>/dev/tcp/$host/$port" 2>/dev/null; then
emit "tcp.$host.$port" open
else
emit "tcp.$host.$port" blocked-or-slow
fi
}
probe_port 1.1.1.1 443
probe_port 1.1.1.1 25
probe_port 8.8.8.8 53
Two notes, because honesty beats a clean demo. The /dev/tcp trick is a bash feature, so sh will not run it. And paste -sd, - is GNU-flavored; on other systems, drop that line.
How I actually run the comparison
Short loop. No dashboards.
- Run the probe locally:
bash parity_probe.sh > local.txt - Run it on the free server:
ssh box 'bash -s' < parity_probe.sh > remote.txt— or paste it into a console tab if you have no SSH. - Diff them:
diff -u local.txt remote.txt - Read the diff in priority order:
os.arch,os.libc,python.ver, thentcp.*, then everything else. - Fix one line in the diff. Re-run. Do not fix five.
I also run the probe on the free server option that MonkeyCode advertises, because I want the probe to run somewhere disposable rather than on my laptop. I use its free model access for one narrow job: summarizing a long diff into a short list of suspicious lines. That is triage, not debugging. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The free server option is convenient for exactly this: a box I do not care about. I do not treat it as a benchmark target. I do not assume it behaves like paid infrastructure, and I do not assume the free tier is permanent. Check the current docs before you rely on anything there.
Which myth bites whom
| Symptom | Myth you probably believe | First check |
|---|---|---|
| Native build fails only remotely | "Same lockfile, same result" |
os.arch, os.libc
|
| Timings look impossible | "Clocks agree" |
http.date vs date.utc
|
| Cache misses after idle | "The disk persists" |
cwd.writable, restart behavior |
| Mail or webhook never arrives | "Network is open" |
tcp.* lines |
| Odd text encoding bugs | "Locale does not matter" | locale |
What this workflow cannot tell you
It cannot tell you how fast the box is. It reads facts, not throughput.
It cannot prove a firewall policy is stable. Ports change, and policies change with them.
It cannot replace a real container image or a lockfile-based deploy. It is a first-aid step, not a build system.
And it cannot catch what you forgot to probe. If a dependency needs a kernel feature, add that check yourself.
Who should skip this
If you already ship an immutable image with pinned base layers, you have solved this better than I have.
If your workload needs long-lived daemons, GPUs, or strict isolation guarantees, a free box is the wrong tool. Use real infrastructure and skip the diff.
If you need byte-exact reproducibility for compliance, a shell probe will not satisfy the auditor.
The takeaway I keep relearning
Most "works on my machine" bugs are environment questions wearing a code costume.
So ask the environment question first. Then read the stack trace.
If you only take one thing: write the diff before you write the fix. A two-minute probe beats an hour of guessing, and it costs you nothing but a text file.
Top comments (0)