A test or build passes on your laptop and fails in CI with no code difference — the classic "works on my machine" symptom that runtime parity frameworks exist to eliminate.

Diagnostic

Capture a normalized runtime fingerprint in both environments and diff them. Run this script locally and as a CI step, then compare the two outputs.

#!/usr/bin/env bash
set -euo pipefail
# runtime-fingerprint.sh — emit a deterministic snapshot of the runtime.
{
  echo "arch=$(uname -m)"
  echo "kernel=$(uname -s)"
  echo "node=$(node -v 2>/dev/null || echo none)"
  echo "python=$(python3 -V 2>&1 || echo none)"
  echo "tz=$(date +%Z)"
  echo "lc_all=${LC_ALL:-unset}"
  echo "lang=${LANG:-unset}"
  echo "node_env=${NODE_ENV:-unset}"
} | sort

Expected BAD output — diffing local (left) against CI (right) reveals the divergent rows:

$ diff <(./runtime-fingerprint.sh) ci-fingerprint.txt
< arch=arm64
> arch=x86_64
< node=v20.18.0
> node=v18.20.4
< tz=CEST
> tz=UTC
< lang=en_US.UTF-8
> lang=C

Root cause

"Works on my machine" is almost never the code; it is an undeclared dependency on the runtime. The four usual culprits are a different language/runtime version (a Array.prototype method or f-string feature present locally but not in CI), divergent environment variables (a NODE_ENV or feature flag set in your shell but absent on the runner), and locale/timezone drift — string sorting, number parsing, and date formatting all change with LANG/LC_ALL/TZ, so a test that sorts names or formats a timestamp passes under en_US.UTF-8/CEST and fails under C/UTC. CPU architecture adds a fifth: arm64 locally versus x86_64 in CI exposes native-module and floating-point differences. None of these live in the repo, so the diff is invisible until you fingerprint both sides. Pinning each axis turns the runtime into a declared, version-controlled dependency.

Resolution

  1. Pin the language/runtime version so both sides resolve identically.
  2. Declare every required env var in .env.example and inject the same set into CI.
  3. Force locale and timezone explicitly in both environments.
  4. Pin the container platform so architecture matches the CI runner.

Pin runtime versions in a manifest both the workstation and CI consume:

# .tool-versions (asdf/mise) — single source for local and CI
nodejs 20.18.0
python 3.12.4

Normalize locale, timezone, and architecture in Compose so local execution mirrors the runner:

# docker-compose.yml
services:
  app:
    build: .
    platform: linux/amd64   # match CI's x86_64 runners
    environment:
      - LANG=C.UTF-8
      - LC_ALL=C.UTF-8
      - TZ=UTC
      - NODE_ENV=test

Validate the env contract so a missing variable fails loudly rather than drifting silently — see catching missing env vars before container startup:

#!/usr/bin/env bash
set -euo pipefail
# Assert every key declared in .env.example is exported before running tests.
while IFS= read -r key; do
  [ -z "$key" ] && continue
  [ -n "${!key:-}" ] || { echo "missing env var: $key"; exit 1; }
done < <(grep -vE '^\s*#|^\s*$' .env.example | cut -d= -f1)
echo "env contract satisfied"

Expected output

Once each axis is pinned, the fingerprints match and the diff is empty:

$ diff <(./runtime-fingerprint.sh) ci-fingerprint.txt
$ echo $?
0

The previously CI-only failure now reproduces (and passes) identically in both places, confirming parity rather than coincidence.

Prevention

  1. Run runtime-fingerprint.sh in CI and fail the job if it diverges from a committed baseline — this is the lightweight companion to automating runtime parity checks between local and staging.
  2. Keep the .tool-versions pin and the Compose platform/locale block under review in every PR.
  3. Add the env-contract assertion to your onboarding health-check script so drift surfaces before tests run.

macOS (Docker Desktop): the host TZ and LANG come from system settings, not the container; never assume the host locale — always set TZ/LC_ALL inside Compose. WSL2: the WSL distro often defaults LANG to C.UTF-8 while Windows is en_US; pin locale explicitly so WSL and CI agree. Apple Silicon (ARM64): without platform: linux/amd64, native modules compile for arm64 and behave differently than CI's x86_64 — pin the platform to reproduce CI faithfully.

Rollback

#!/usr/bin/env bash
set -euo pipefail
git checkout -- .tool-versions docker-compose.yml   # revert pins if they break local-only workflows