Update (January 17, 2026): Based on feedback from Jan Tymiński and others, several clarifications have been added to distinguish deployment consistency from execution determinism, and to better acknowledge Docker's orchestration benefits. The core thesis remains unchanged.
Docker solved a real problem. Before containers, deploying software meant hoping the production environment matched development. Library versions drifted. System configurations diverged. “Works on my machine” became a punchline because it happened constantly.
Containers fixed this by shipping everything: application, dependencies, libraries, and a slice of the operating system. If the container works in testing, it works in production, because production receives the same bytes.
This is a genuine engineering achievement. It has made deployment dramatically more reliable for millions of applications. But it’s worth examining what Docker actually guarantees—and what it doesn’t—because the answer reveals a hidden cost that many teams never consider.
What Docker Guarantees
Docker guarantees filesystem consistency. When you build an image, you capture a specific state: these files, with these contents, at these paths. When you run the image, you get that filesystem. The application sees the same libraries, the same configuration files, the same directory structure, regardless of the host.
This eliminates an entire class of deployment failures. No more “missing libssl” errors. No more “wrong Python version” surprises. No more configuration drift between staging and production.
Docker also provides process isolation. Your container gets its own process namespace, network namespace, and (optionally) resource limits. Other processes on the host cannot interfere with your files or memory.
These guarantees are valuable. They are also the full extent of what containers provide at the deployment layer.
What Docker Does Not Guarantee
Docker does not guarantee deterministic execution. The same container, given the same inputs, may produce different outputs on different runs. This is not a bug in Docker—it’s a fundamental limitation of what filesystem consistency can achieve.
Important distinction: Docker solves deployment consistency (the application starts in the same state across environments). It does not solve execution determinism (the application executes identically given the same inputs). These are different problems at different layers of the stack. A static binary also doesn’t guarantee execution determinism unless the code itself is written deterministically. The difference is that Docker solves deployment problems, while deterministic code design solves execution problems.
Consider a container running a multi-threaded application. The threads are scheduled by the host kernel, not by Docker. The order of thread execution depends on CPU load, interrupt timing, and scheduler decisions that vary from moment to moment. Two runs of the same container, with the same inputs, may interleave threads differently and produce different results.
Consider a container that calls gettimeofday() or reads /dev/urandom. The values returned depend on the host system’s state at the moment of the call. The container filesystem is identical; the execution is not.
Consider a container that allocates memory with malloc(). The addresses returned depend on the host’s memory layout, ASLR settings, and prior allocations. Code that accidentally depends on pointer ordering will behave differently across runs.
Docker guarantees that your code will start in the same state. It does not guarantee that your code will execute the same way or finish in the same state.
The 450MB Question
A typical Docker image for a modest application weighs hundreds of megabytes. A Python web service might ship 450MB: the application code, Python runtime, system libraries, and a Debian or Alpine base image.
Of those 450MB, how much is actually your code? Often, less than one percent. The application itself might be a few hundred kilobytes. The rest is infrastructure: someone else’s code, someone else’s configuration, someone else’s decisions about how to structure a Linux filesystem.
This inversion—shipping 16,000 times more code than you wrote—is the hidden cost of the Docker approach. You’re not shipping your application. You’re shipping an entire operating system environment, frozen at a point in time, because your application cannot be trusted to run consistently without it.
This comparison is deliberately rhetorical to highlight the cost difference. In practice:
- Optimized containers (multi-stage builds, Alpine/scratch base) can be 10-50MB
- Static binaries with realistic dependencies can be 5-20MB
- The ratio changes, but the principle remains: Docker packages environment, static binaries eliminate environmental dependencies
Both approaches have contexts where they’re optimal. The point is not “Docker is bad” but “understand what you’re trading.”
The question is: why can’t your application run without this packaging?
Inconsistency Has a Source
Software behaves inconsistently across environments for specific, identifiable reasons:
| Source | Docker Helps? | Why? |
|---|---|---|
| Dynamic linking | ✅ | Ships consistent library versions |
| Configuration files | ✅ | Ships consistent config files |
| System calls (time, random) | ❌ | Not a deployment problem - needs code changes |
| Non-deterministic algorithms | ❌ | Not a deployment problem - needs code changes |
| Thread scheduling | ❌ | Kernel-level, outside Docker’s scope |
Dynamic linking. Your code calls functions in shared libraries. Different library versions implement those functions differently. The fix: ship the library (Docker’s approach) or eliminate the dependency (static linking).
Configuration files. Your code reads settings from files that differ between environments. The fix: ship the files (Docker’s approach) or embed configuration in the binary.
System calls with environmental dependencies. Your code asks the operating system for information—time, randomness, process IDs—that varies by host. The fix: there is no filesystem fix. Docker cannot help here—this requires code-level changes.
Non-deterministic algorithms. Your code uses hash tables with random iteration order, threading with undefined interleaving, or memory allocation with unpredictable addresses. The fix: there is no container fix. The non-determinism is in your code.
Docker addresses the first two sources by brute force: ship everything, and the filesystem becomes consistent. But it cannot address the latter three, because they are not filesystem problems. They are execution problems that require different solutions.
The Deterministic Alternative
What if, instead of shipping an environment that tolerates inconsistent code, you wrote code that executes consistently?
The c-from-scratch project demonstrates this approach. The Baseline module—a statistical anomaly detector—compiles to a 27KB static binary. It has no dynamic library dependencies. It reads no configuration files. It makes no system calls except through a narrow POSIX interface (read, write, basic file operations).
This binary produces identical output on:
- A developer’s MacBook
- A Raspberry Pi running Raspbian
- An x86 server running RHEL
- A CI runner in GitHub Actions
- A Docker container (if you really want one)
The consistency does not come from shipping a consistent environment. It comes from writing code that does not depend on environmental details. The POSIX interface is the same everywhere. The binary carries its own logic. Given the same input file, it produces the same output file, byte for byte, regardless of where it runs.
What Determinism Requires
Writing deterministic code is not free. It requires discipline in several areas:
Static linking. Dependencies are compiled into the binary, not resolved at runtime. This increases binary size (though rarely to container scale) and requires recompilation for security updates. The tradeoff: you know exactly what code will execute.
No environmental queries. The code does not ask “what time is it?” or “give me a random number” unless those values are provided as explicit inputs. This makes testing easier—inputs are controlled—but requires architectural changes if your current design assumes ambient access to time or randomness.
Deterministic algorithms. Hash tables use stable iteration order. Floating-point operations are ordered consistently. Threading, if present, uses explicit synchronisation that produces the same interleaving on every run. This is the hardest requirement—it touches algorithmic choices throughout the codebase.
Narrow system interface. The code interacts with the operating system through a minimal, well-defined interface. POSIX provides this: open, read, write, close. The less you ask of the OS, the less can vary between hosts.
These requirements are achievable. They are not even unusual in certain domains. Safety-critical systems—avionics, medical devices, automotive controllers—routinely meet them because certification demands reproducible behaviour. The techniques exist; they’re simply not applied to most software because most software doesn’t need them.
When Containers Make Sense
To be clear: Docker is not wrong. Containers are the right solution for many, perhaps most, deployment scenarios.
If your application genuinely requires a complex runtime—Python with specific package versions, Node.js with native extensions, Java with particular JVM flags—containers package that complexity efficiently. The alternative (documenting and reproducing the environment manually) is error-prone and tedious.
If your deployment target is heterogeneous—some hosts run Ubuntu, others CentOS, others Amazon Linux—containers provide a common abstraction. Your application sees the same filesystem regardless of the underlying host distribution.
If your team lacks control over the deployment environment—you’re shipping to customer data centres, or running in a managed platform like Kubernetes—containers are the interface that platform expects.
If your application’s non-determinism is acceptable—most web services don’t need bit-identical reproducibility—then Docker’s guarantees are sufficient, and its costs are reasonable.
Additionally, container orchestration platforms (Kubernetes, ECS, Cloud Run) provide operational benefits that go beyond deployment consistency:
- Automatic scaling and load balancing
- Rolling updates and rollbacks
- Health checks and self-healing
- Service discovery and networking
- Resource isolation and multi-tenancy
These operational benefits often justify containers even for applications that could run as static binaries. The infrastructure value exceeds the deployment value.
When Containers Are Overhead
But there are cases where containers add cost without adding value:
Deterministic applications. If your code already produces identical results across environments, the container is packaging megabytes of redundant insurance. A static binary does not need an environmental safety blanket.
Resource-constrained deployments. Edge devices, embedded systems, and IoT devices often lack the storage and memory for container runtimes. A static binary runs directly; a container requires infrastructure.
Certification-required systems. Safety-critical domains require evidence of exactly what code executes. A container image contains thousands of packages, each a potential audit target. A static binary contains your code and nothing else. The certification surface area differs by orders of magnitude.
High-frequency deployments. If you deploy hundreds of times per day, container image transfer becomes a bottleneck. A small binary transfers in milliseconds; a large image takes minutes over slow links.
Debugging production issues. When a container behaves differently in production than in testing—despite identical images—the problem is non-determinism in your code or environment. The container obscured this; it did not prevent it.
The Real Question
The debate is not "containers versus no containers." It's "where does consistency come from?"
Docker’s answer: consistency comes from packaging. Ship the environment, and the environment will be consistent.
The deterministic answer: consistency comes from code. Write code that does not depend on environmental details, and the environment becomes irrelevant.
Both approaches work. Both have costs. Docker’s cost is visible in image sizes, transfer times, and registry bills. Determinism’s cost is invisible in engineering discipline, architectural constraints, and upfront design effort.
The question is which cost you’d rather pay—and whether you’re paying Docker’s cost because you need to, or because you never considered the alternative.
Shipping a Bet
A colleague once described Docker images as “shipping a bet that nothing upstream breaks.” The image captures a moment: these library versions work together today. But libraries are maintained by other people, with other priorities, on other timelines.
When a CVE arrives for a transitive dependency four layers deep in your container, you rebuild the image—and pray that the new versions still work together. Usually they do. Sometimes they don’t, and you spend days debugging incompatibilities that your code did not introduce.
The deterministic approach does not eliminate this risk entirely. Static binaries still depend on some system calls, and operating systems still receive updates. But the surface area is radically smaller. You depend on POSIX semantics, not on the internal implementation of libfoo version 2.3.4-ubuntu7.
Smaller bets are easier to win.
Conclusion
Docker solved deployment inconsistency through containment: wrap your application in an environment, and ship the whole thing. This works. It has transformed how software is deployed, making reliable delivery accessible to teams that previously struggled with environmental chaos.
But containment is not the only solution. Elimination is another. If your code does not depend on environmental details—if the same binary produces the same output on any POSIX system—then the environment does not need to be shipped. The consistency is in the code, not the container.
It’s worth noting that containers and deterministic code are not mutually exclusive. You can ship a deterministically-designed application in a container to get both deployment consistency AND execution determinism. The question is whether you need the container layer for your specific use case, or whether simpler deployment (static binary, systemd service, etc.) suffices.
The 450MB container and the 27KB binary both represent answers to the question “how do we deploy reliably?” One ships everything to avoid thinking about dependencies. The other thinks carefully about dependencies to avoid shipping everything.
Neither is universally correct. But if you’ve only considered the first approach, it may be worth examining the second. The discipline required to write deterministic code pays dividends beyond deployment: testability, debuggability, reproducibility, and certifiability.
The hidden cost of consistent environments is the assumption that your code requires them. Sometimes it does. Sometimes—with care and discipline—it doesn’t.
As with any architectural approach, suitability depends on system requirements, deployment constraints, and team capabilities. But the next time you wait for a large image to transfer, it’s worth asking: what, exactly, am I shipping?
Thanks to Jan Tymiński for thoughtful feedback that improved the clarity of this article’s distinction between deployment and execution layers.