The 'Works on My Machine' Problem
Explore how non-hermetic builds cause inconsistencies across developer machines and CI environments.
The 'Works on My Machine' Problem
Master hermetic builds with free flashcards and spaced repetition practice. This lesson covers the notorious "works on my machine" syndrome, environmental inconsistencies, and reproducibility challengesβessential concepts for understanding why hermetic builds matter in modern software development.
Welcome π
Every developer has experienced it: you write code that runs perfectly on your computer, but when a teammate tries to run itβdisaster. Tests fail, features break, or worse, the application won't even start. When you ask what went wrong, they shrug and say, "I don't know, it just doesn't work." You respond with the most frustrating phrase in software development: "But it works on my machine!" π»β
This seemingly simple problem has cost the software industry billions of dollars in lost productivity, delayed releases, and emergency hotfixes. The "works on my machine" problem isn't just an inconvenienceβit's a fundamental challenge that reveals the hidden complexities of software environments and the desperate need for hermetic builds.
Core Concepts: Understanding Environmental Chaos πͺοΈ
What is the "Works on My Machine" Problem?
The "works on my machine" problem (also called the "WOMM problem") occurs when software behaves differently across different environments, even when the source code is identical. This happens because software doesn't run in a vacuumβit depends on a complex ecosystem of factors that vary from machine to machine.
Key definition: An environment in software development includes everything needed to build and run software: operating system, installed libraries, system configuration, environment variables, file system state, network conditions, and countless other variables.
The Anatomy of Environmental Differences π¬
Let's break down the common sources of environmental inconsistency:
| Environmental Factor | What Varies | Common Symptom |
|---|---|---|
| Dependencies | Library versions, package installations | "Module not found" or "incompatible version" |
| System Configuration | Environment variables, PATH settings | "Command not found" or wrong tool version used |
| Operating System | Windows vs macOS vs Linux differences | File path separators, line endings, case sensitivity |
| File System State | Cached files, temporary data, absolute paths | "Build uses stale data" or "hardcoded path fails" |
| Network Access | Available registries, download speeds, firewalls | "Timeout fetching dependency" or wrong package version |
| Time & Locale | System time, timezone, language settings | "Date parsing fails" or encoding issues |
Why Does This Matter? The Reproducibility Crisis π
Reproducibility means that given the same inputs, a process produces the same outputs every time. In software builds, this means:
β Reproducible build: Same source code β Same binary output (always) β Non-reproducible build: Same source code β Different binary outputs (chaos!)
When builds aren't reproducible, you face:
- Debugging nightmares: "I can't reproduce the bug you're seeing"
- Deployment disasters: "It worked in staging but failed in production"
- Security vulnerabilities: "We can't verify this binary came from our source code"
- Team friction: "Just works for me, not sure what you're doing wrong"
- Wasted time: Hours spent on "environmental archaeology" instead of building features
π‘ Did you know? The Debian project found that only 83% of packages could be built reproducibly in 2015. By implementing hermetic build practices, they improved this to over 95% by 2021.
The Hidden Dependencies Problem π΅οΈ
Many builds have hidden dependenciesβthings your code relies on that aren't explicitly declared:
VISIBLE VS HIDDEN DEPENDENCIES βββββββββββββββββββββββββββββββββββββββ β π¦ Your Project β β β β β package.json (visible) β β ββ react: 18.2.0 β β ββ lodash: 4.17.21 β β β β β Hidden dependencies: β β ββ Node.js version (16? 18?) β β ββ npm vs yarn vs pnpm β β ββ Global npm packages β β ββ System Python (some deps) β β ββ $PATH and $NODE_ENV vars β β ββ OS architecture (x64? ARM?) β β ββ Cached node_modules state β βββββββββββββββββββββββββββββββββββββββ
These hidden dependencies are the primary cause of the "works on my machine" problem. Your machine has them configured correctly (often by accident!), but your teammate's machine doesn't.
Real-World Examples: When Good Code Goes Bad π
Example 1: The Python Path Mystery π
Scenario: Sarah writes a Python script that imports a custom utility module.
Sarah's machine (works):
## main.py
import utils
result = utils.process_data([1, 2, 3])
print(result)
What Sarah doesn't realize:
- She has
export PYTHONPATH=/home/sarah/projects/libin her.bashrc - The
utils.pyfile is in that directory - Python finds it automatically on her machine
Jake's machine (fails):
$ python main.py
Traceback (most recent call last):
File "main.py", line 1, in <module>
import utils
ModuleNotFoundError: No module named 'utils'
Jake doesn't have that PYTHONPATH set, so Python can't find the module.
The hermetic solution: Make the dependency explicit:
## Instead of relying on PYTHONPATH:
## 1. Put utils.py in the project directory
## 2. Use relative imports
## 3. Create a proper package with setup.py
## 4. Document ALL requirements including Python version
Example 2: The Node.js Version Roulette π°
Scenario: A team builds a web application that works perfectly in development.
Developer's machine:
- Node.js v18.12.0 (latest LTS)
- Modern JavaScript features work perfectly
CI/CD server:
- Node.js v14.17.0 (older version)
- Build fails with syntax errors
// Code using optional chaining (Node 14+ feature)
const name = user?.profile?.name ?? 'Anonymous';
// Works on Node 18 β
// Crashes on Node 12 β (Unexpected token '?')
Why it happened:
- No
package.jsonengine specification - Developer accidentally used their global Node installation
- CI server had old Node version pinned
The hermetic solution:
{
"name": "my-app",
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"scripts": {
"preinstall": "node -e \"if(process.version<'v18')throw new Error('Need Node 18+')\""
}
}
Use tools like nvm (Node Version Manager) or Docker to ensure everyone uses the same Node version.
Example 3: The Operating System Shuffle πΏ
Scenario: A build script works on Linux but fails on Windows.
Linux (developer's machine):
#!/bin/bash
## build.sh
find src/ -name "*.ts" | xargs tsc
cat dist/*.js > bundle.js
Windows (colleague's machine):
'find' is not recognized as an internal or external command
'cat' is not recognized as an internal or external command
The problems:
- Shell script syntax (Bash vs PowerShell vs Command Prompt)
- Different path separators (
/vs\) - Different line endings (LF vs CRLF)
- Different available commands
The hermetic solution: Use cross-platform tools:
// build.js (Node.js - runs everywhere)
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Cross-platform path handling
const srcDir = path.join(__dirname, 'src');
// Use Node.js APIs instead of shell commands
const files = fs.readdirSync(srcDir)
.filter(f => f.endsWith('.ts'))
.map(f => path.join(srcDir, f));
// Use a cross-platform build tool
execSync(`tsc ${files.join(' ')}`);
Example 4: The Dependency Time Bomb β°
Scenario: Code that worked yesterday suddenly breaks today, with no changes to your codebase.
Yesterday:
// package.json
{
"dependencies": {
"some-library": "^2.1.0"
}
}
What happened overnight:
- Library maintainer published version 2.2.0
- It has a breaking API change (accidentally or intentionally)
- Your
^2.1.0means "any version 2.x.x is okay" - New developer runs
npm installand gets 2.2.0 - Everything breaks
The hermetic solution:
- Use exact versions (remove the
^):
{
"dependencies": {
"some-library": "2.1.0"
}
}
- Commit your lock file (
package-lock.json,yarn.lock, etc.):
## .gitignore - WRONG!
package-lock.json β Don't ignore this!
## Instead, commit it:
git add package-lock.json β
- Use a private registry or vendoring to control when updates happen
Why Traditional Solutions Fall Short β οΈ
Many teams try to solve the "works on my machine" problem with documentation:
The "README solution":
## Setup Instructions
1. Install Node.js 18
2. Install Python 3.9
3. Set JAVA_HOME to your JDK installation
4. Install these global npm packages: ...
5. Configure your PATH to include: ...
6. Run these 15 commands in order: ...
Why this fails:
- π Documentation drift: README gets outdated as project evolves
- π₯ Human error: Developer skips a step or does it wrong
- π Not repeatable: Manual steps = inconsistent results
- β° Time-consuming: Hours to set up on each new machine
- π Debugging hell: When it fails, which step went wrong?
The Path to Hermeticity π―
The "works on my machine" problem is fundamentally a problem of environmental couplingβyour build is too tightly coupled to the specific state of one developer's machine.
Hermetic builds solve this by:
ββββββββββββββββββββββββββββββββββββββββββββ β TRADITIONAL BUILD (coupled) β β β β Source Code βββ¬βββ Dev Machine A βββ β β β β β β ββββ Dev Machine B βββ β β β β β β ββββ CI Server ββββββ β β ββββββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββ β HERMETIC BUILD (isolated) β β β β Source Code βββ¬βββ Isolated Env βββ β β β + β (same) β β All Deps ββββ Isolated Env βββ β β β Explicitly β (same) β β Declared ββββ Isolated Env βββ β β β β β Same inputs β Same environment β β β Same output (always!) β ββββββββββββββββββββββββββββββββββββββββββββ
Key principles of hermetic builds:
- π Isolation: Build doesn't access anything outside its declared inputs
- π Explicit dependencies: All requirements are declared and versioned
- π Reproducibility: Same inputs β same outputs (always)
- β±οΈ Determinism: No randomness, timestamps, or network-dependent behavior
- π« No side effects: Build doesn't modify system state
π‘ Think of it like this: A hermetic build is like a pure function in programmingβgiven the same inputs, it always produces the same output, with no hidden dependencies or side effects.
Common Mistakes to Avoid β οΈ
Mistake 1: Relying on Global Installations β
Wrong:
## Assuming user has globally installed tools
npm install -g typescript
tsc src/index.ts
Right:
## Project-local dependencies
npm install --save-dev typescript
npx tsc src/index.ts # Uses local version
Mistake 2: Not Version-Locking Dependencies β
Wrong:
{
"dependencies": {
"react": "*", // Any version! π±
"lodash": "^4.17.0" // Could get 4.99.0 tomorrow
}
}
Right:
{
"dependencies": {
"react": "18.2.0",
"lodash": "4.17.21"
}
}
// AND commit your lock file!
Mistake 3: Hardcoding Absolute Paths β
Wrong:
## Hardcoded path to YOUR machine
config = open('/Users/sarah/projects/myapp/config.json')
Right:
import os
from pathlib import Path
## Relative to script location
config_path = Path(__file__).parent / 'config.json'
config = open(config_path)
Mistake 4: Depending on Network During Build β
Wrong:
## Build fetches from internet (network failure = build failure)
curl https://example.com/asset.js > vendor.js
Right:
## Assets committed to repo or fetched once and cached
## Build uses local copies only
cp vendor/asset.js dist/
Mistake 5: Using Timestamps or Random Values β
Wrong:
// Different output every time!
const buildId = Date.now();
fs.writeFileSync('build.json', JSON.stringify({ buildId }));
Right:
// Deterministic identifier from git commit
const buildId = execSync('git rev-parse HEAD').toString().trim();
fs.writeFileSync('build.json', JSON.stringify({ buildId }));
Key Takeaways π
β The "works on my machine" problem occurs when software behaves differently across environments due to environmental inconsistencies
β Hidden dependencies (environment variables, global tools, system configuration) are the primary culprit
β Reproducibility means same inputs β same outputs, which is essential for reliable software development
β Documentation alone is insufficientβhuman error and drift make manual setup unreliable
β Hermetic builds solve this by isolating the build environment and making all dependencies explicit
β Key hermetic principles: Isolation, explicit dependencies, reproducibility, determinism, no side effects
Quick Reference Card π
π WOMM Problem Checklist
| β Explicit dependencies | All tools and libraries declared with versions |
| β Lock files committed | package-lock.json, Pipfile.lock, etc. in version control |
| β Local tooling | No reliance on global installations |
| β Relative paths | No hardcoded absolute paths to specific machines |
| β Cross-platform code | Works on Windows, macOS, Linux |
| β Deterministic builds | No timestamps, random values, or network dependencies |
| β Documented requirements | Clear system requirements (OS version, architecture) |
| β Automated setup | Scripts/containers for consistent environment creation |
π€ Did You Know?
The phrase "works on my machine" became so notorious in software development that it spawned countless memes, t-shirts, and even a satirical certification badge that says "Works on My Machine - Certified." Docker, one of the most popular containerization tools, directly addresses this problem with their tagline: "But it works on my machine... then we'll ship your machine!"
π Further Study
- Reproducible Builds Project - Community documentation on achieving byte-for-byte reproducible builds
- Docker Documentation: Understanding the Build Context - How containerization solves environmental inconsistency
- Bazel Build System - Google's hermetic build tool documentation
Next in the roadmap: You'll learn about the specific properties that make a build truly hermetic and how to achieve them in your projects.