I've seen countless attempts to replace "docker build" and Dockerfile. They often want to give tighter control to the build, sometimes tightly binding to a package manager. But the Dockerfile has continued because of its flexibility. Starting from a known filesystem/distribution, copying some files in, and then running arbitrary commands within that filesystem mirrored so nicely what operations has been doing for a long time. And as ugly as that flexibility is, I think it will remain the dominant solution for quite a while longer.
> But the Dockerfile has continued because of its flexibility.
The flip side is that the world still hasn’t settled on a language-neutral build tool that works for all languages. Therefore we resort to running arbitrary commands to invoke language-specific package managers. In an alternate timeline where everyone uses Nix or Bazel or some such, docker build would be laughed out of the window.
As a Nix evangelist, I have to say: Nix is really not capable of replacing languag-specific package managers.
> running arbitrary commands to invoke language-specific package managers.
This is exactly what we do in Nix. You see this everywhere in nixpkgs.
What sets apart Nix from docker is not that it works well at a finer granularity, i.e. source-file-level, but that it has real hermeticity and thus reliable caching. That is, we also run arbitrary commands, but they don't get to talk to the internet and thus don't get to e.g. `apt update`.
In a Dockerfile, you can `apt update` all you want, and this makes the build layer cache a very leaky abstraction. This is merely an annoyance when working on an individual container build but would be a complete dealbreaker at linux-distro-scale, which is what Nix operates at.
Fundamentally speaking, the key point is really just hermeticity and reliable caching. Running arbitrary commands is never the problem anyways. What makes gcc a blessed command but the compiler for my own language an "arbitrary" command anyways?
And in languages with insufficient abstraction power like C and Go, you often need to invoke a code generation tool to generate the sources; that's an extremely arbitrary command. These are just non-problems if you have hermetic builds and reliable caching.
I mean, I guess at a theoretical level. In practice, it's just not a large problem.
Well, arbitrary granularity is possible with Nix, but the build systems of today simply do not utilise it. I've for example written an experimental C build system for Nix which handles all compiler orchestration and it works great, you get minimal recompilations and free distributed builds. It would be awesome if something like this was actually available for major languages (Rust?). Let me know if you're working on or have seen anything like this!
A problem with that is that Nix is slow.
On my nixos-rebuild, building a simple config file for in /etc takes much longer than a common gcc invocation to compile a C file. I suspect that is due to something in Nix's Linux sandbox setup being slow, or at least I remember some issue discussions around that; I think the worst part of that got improved but it's still quite slow today.
Because of that, it's much faster to do N build steps inside 1 nix build sandbox, than the other way around.
Another issue is that some programming languages have build systems that are better than the "oneshot" compilation used by most programming languages (one compiler invocation per file producing one object file, e.g. ` gcc x.c x.o`). For example, Haskell has `ghc --make` which compiles the whole project in one compiler invocation, with very smart recompilation avoidance (pet-function, comment changes don't affect compilation, etc) and avoidance of repeat steps (e.g. parsing/deserialising inputs to a module's compilation only once and keeping them in memory) and compiler startup cost.
Combining that with per-file general-purpose hermetic build systems is difficult and currently not implemented anywhere as far as I can tell.
To get something similar with Nix, the language-specific build system would have to invoke Nix in a very fine-grained way, e.g. to get "avoidance of codegen if only a comment changed", Nix would have to be invoked at each of the parser/desugar/codegen parts of the compiler.
I guess a solution to that is to make the oneshot mode much faster by better serialisation caching.
What if you set up a sandbox pool? Maybe I'm rambling, I haven't read much Nix source code, but that should allow for only a couple of milliseconds of latency on these types of builds. I have considered forking Nix to make this work, but in my testing with my experimental build system, I never experienced much latency in builds. The trick to reduce latency in development builds is to forcibly disable the network lookups which normally happen before Nix starts building a derivation:
preferLocalBuild = true;
allowSubstitutes = false;
Set these in each derivation. The most impactful thing you could do in a Nix fork according to my testing in this case is to build derivations preemptively while you are fetching substitutes and caches simultaneously, instead of doing it in order.If you are interested in seeing my experiment, it's open on your favourite forge:
I use crane, but it does not have arbitrary granularity. The end goal would be something which handled all builds in Nix.
Reminds me of the “Electric cars in reverse” video where the guy envisions a world where all vehicles are electric and tries to make the argument for gas engines.
Link?
try searching for 'Rory Sutherland: What If Petrol Cars Were Invented In 2025'
Actual article was in the evening standard, but like all things Rory Sutherland, it’s worth to watch him tell the story: https://youtu.be/OTOKws45kCo?si=jbTdx3YCGkZv3Akb
For those who want more of him, check out his classic TED talk from decades ago: “Lessons from an ad man”
https://www.ted.com/talks/rory_sutherland_life_lessons_from_...
There is some truth to it, however in production it is simple: There is a working deployment or not.
Therefore I would rephrase your remarks as upside: let others continue scratch their head while others deploy working code to PROD.
I am glad there is a solution like Docker - with all it flaws. Nothing is flawless, there is always just yet another sub-optimal solution weighting out the others by a large margin.
Popularity of a technology usually isn’t perfectly correlated with how good it is.
> let others continue scratch their head while others deploy working code to PROD.
You make it sound like when docker build arrived on the scene, a cross-language hermetic build tool was still a research project. That’s just untrue.
This calls for https://xkcd.com/927/.
There are some hurdles preventing that flow from achieving reproducible builds. As the bad guys get more sophisticated, it's going to become more and more important that one party can say "we trust this build hash" and a separate party to say "us too".
That's not going to work if both parties get different hashes when they build the image, which won't happen as long as file modification timestamps (and other such hazards) are part of what gets hashed.
Recent versions of buildkit have added support for SOURCE_DATE_EPOC. I've been making the images reproducible before that with my own tooling, regctl image mod [1] to backdate the timestamps.
It's not just the timestamps you need to worry about. Tar needs to be consistent with the uid vs username, gzip compression depends on implementations and settings, and the json encoding can vary by implementation.
And all this assumes the commands being run are reproducible themselves. One issue I encountered there was how alpine tracks their package install state from apk, which is a tar file that includes timestamps. There are also timestamps in logs. Not to mention installing packages needs to pin those package versions.
All of this is hard, and the Dockerfile didn't make it easy, but it is possible. With the right tools installed, reproducing my own images has a documented process [2].
> I've been making the images reproducible before that with my own tooling
I've been doing the same, using https://github.com/reproducible-containers/repro-sources-lis... . It allows you to precisely pin the state of the distro package sources in your Docker image, using snapshot.ubuntu.com & friends, so that you can fearlessly do `apt-get update && apt-get install XYZ`.
Does any of that matter if you’re not auditing the packages you install?
I’m more concerned about sources being poisoned over the build processes. Xz is a great example of this.
Both are needed, but you get more bang for your buck focusing on build security than on audited sources. If the build is solid then it forces attackers to work in the open where all auditors can work together towards spoiling the attack.
If you flip it around and instead have magically audited source but a shaky build, then perhaps a diligent user can protect themself, but they do so by doing their own builds, which means they're unaware that the attack even exists. This allows the attacker to just keep trying until they compromise somebody who is less diligent.
Getting caught requires a user who analyses downloaded binaries in something like ghidra... who does that when it's much easier to just build them from sources instead? (answer: vanishingly few people). And even once the attacker is found out, they can just hide the same payload a bit differently, and the scanners will stop finding it again.
Also, "maybe the code itself is malicious" can only ever be solved the hard way, whereas we have a reasonable hope of someday providing an easy solution to the "maybe the build is malicious" problem.
I'm not sure if I was just holding it wrong, but I couldn't create images reproducibly using Docker. (I could get this working with Podman/buildah however.)
The lack of docker registry-like solutions really does seem to be the chokepoint for many alternatives.
Personally I love using mkosi and while it has all the composability and deployment options I'd care for, its clear not everyone wants to build starting only with a blank set of OS templates.
There are lots of alternative container image registries: quay, harbor, docker's open sourced one, the cloud providers, github, etc.
Or do you mean a replacement for docker hub?
Nix is exceptionally good at making docker containers.
Yes but then you're committed to using Nix which doesn't work so well the moment you need some software not packaged by Nix.
Want to throw a requirements.txt in there? No no, why would you even ask that? Meanwhile docker says yeah sure just run pip install, why should I care?
Then you're committing to maintaining a package for that software.
Like all LLM boosters, you've ignored the fact that the largest time sink in many kinds of software is not initial development, but perpetual maintenance.
It's not materially any different from maintaining lines in a Dockerfile.
It is mateirially different compared to "maintaining" the line 'RUN apt-get -y install foobar'
Is it though? If the way that I’m going to edit those files is by typing the same natural language command into Claude code, and the edit operation to maintain it takes 20 seconds instead of 10, to me that seems pretty materially the same
How so?
This. I wouldn't have touched Nix when you needed someone who was really good at Nix to keep it working, but agents make it viable to use in a number of place.
Packaging for nix is exceptionally easy once you learn it. And once something is packaged, it's solved for all, it's not going to randomly break.
If you care about getting it to work with minimal effort right now more thar about it being sustainable later, then sure.
> Packaging for nix is exceptionally easy once you learn it
Most of the complaints I've seen about Nix about around documentation, so "once you learn it" might be the larger issue.
I don't in ow if I'd say it's "easy". The Python ecosystem in particular is quite hard to get working in a hermetic way (Nix or otherwise). Multiple attempts at getting Python easy to package with Nix have come and gone over the years.
I use software from pretty much every language with Nix. And I package it myself too when needed. Including Python often :)
Packing software with nix is easier than any other system TBH and just seems to be just getting easier.
Nix doesn't make sense if all you're going to use it for is building Docker images. It only makes sense if you're all in in the first place. Then Docker images are free.
Does Nix do one layer per dependency? Does it run into >=128 layers issues?
In Spack [1] we do one layer per package; it's appealing, but I never checked if besides the layer limit it's actually bad for performance when doing filesystem operations.
This post has a great overview: https://grahamc.com/blog/nix-and-layered-docker-images/
tl;dr it will put one package per layer as much as possible, and compress everything else into the final layer. It uses the dependency graph to implement a reasonable heuristic for what is fine grained and what get combined.
That layering algorithm is also configurable, though I couldn’t really understand how to configure it and just wrote my own post processing to optimize layering for my internal use case. I believe I can open source this w/o much work.
The layer layout is just a json file so it can be post processed w/o issue before passing to the nix docker builders
Especially if you use nix2container to take control over the layer construction and caching.
I'm not sure if this is what you mean but in some ways it would be nice to have tighter coupling with a registry. Docker build is kind of like a multiplexer - pull from here or there and build locally, then tag and push somewhere else. Most of the time all pulls are from public registries, push to a single private one and the local image is never used at all.
It seems overly orthogonal for the typical use case but perhaps just not enough of an annoyance for anyone to change it.
[dead]
^ this account has posted nothing but AI generated spam since it was created 6 hours ago
> the Dockerfile has continued because of its flexibility
I wish we had standardized on something other than shell commands, though. Puppet or terraform or something more declarative would have been such a better alternative to “everyone cargo cults ‘RUN apt-get upgrade’ onto the top of their dockerfiles”.
Like, the layer/stage/caching behavior is fine. I just wish the actual execution parts had been standardized using something at a higher level of abstraction than shell.
> Puppet or terraform or something more declarative would have been such a better alternative
Until you need to do something that isn't covered with its DSL, and you extend it with an external command execution declaration... At which point people will just write bash scripts anyway and use your declarative language as a glorified exec.
If you have 90-95% of everyone's needs (installing packages, compiling, putting files) covered in your DSL, and it has strong consistency and declarativeness, it's not that big of a problem if you need an escape hatch from time to time. Terraform, Puppet, Ansible, SaltStack show this pretty well, and the vast majority of them that isn't bash scripts is better and more maintainable than their equivalents in pure bash would be.
The problem is, ironically, that each DSL has its own execution platform, and is not designed for testability. Bash scripts may be hard to maintain, but at least you can write tests for them.
In Azure YAML I had an odd bug because I used succeeded() instead of not(failed()) as a condition. I had no way of testing the pipeline without executing it. And each DSL has its own special set of sharp edges.
At least Bash's common edges are well known.
Docker broke out the build layer into a separate component called BuildKit (see HN discussion recently https://news.ycombinator.com/item?id=47166264).
However, Dockerfiles are so popular because they run shell commands and permit 'socially' extending someone else shell commands; tacking commands onto the end of someone else's shell script is a natural process. /bin/sh is unreasonably effective at doing anything you need to a filesystem, and if the shell exposes a feature, it has probably been used in a Dockerfile somewhere.
Every other solution, especially declarative ones, tend to come up short when _layering_ images quickly and easily. However, I agree they're good if you control the entire declarative spec.
I'd say LLB is the "standard", Dockerfile is just one of human-friendly frontends, but you can always make one yourself or use an alternative. For example, Dagger uses BuildKit directly for building its containers instead of going through a Dockerfile.
Declarative methods existed before Docker for years and they never caught on.
They sounded nice on paper but the work they replaced was somehow more annoying.
I moved over to Docker when it came out because it used shell.
Give https://github.com/project-dalec/dalec a look. It is more declarative. Has explicit abstractions for packages, caching, language level integrations, hermetic builds, source packages, system packages, and minimal containers.
Its a Buildkit frontend, so you still use "docker build".
The more you try and abstract from the OS, the more problems you're going to run into.
Bash is pretty darn abstracted from the OS, though. Puppet vs Bash is more about abstraction relative to the goal.
If your dockerfile says “ensure package X is installed at version Y” that’s a lot clearer (and also more easy to make performant/cached and deterministic) than “apt-get update; apt-get install $transitive-at-specific-version; apt-get install $the-thing-you-need-atspecific-version”. I’m not thrilled at how distro-locked the shell version makes you, and how easy it is for accidental transitive changes to occur too.
But neither of those approaches is at a particularly low abstraction level relative to the OS itself; files and system calls are more or less hidden away in both package-manager-via-bash and puppet/terraform/whatever.
Dockerfile has the flexibility to do what you want though, no? Use a base image with terraform or puppet or opentofu or whatever pre-installed, then your Dockerfile can just run the right command to apply some declarative config file from the build context.
And if you want something weird that's not supported by your particular tool of choice, you have the escape hatch of running arbitrary commands in the Dockerfile.
What more do you want?
The loose integration between the declarative tools and the container build system drags down performance and creates a lot of footguns re: image size and inert declarative-build-system transitive deps left lying around, I’ve found.
Why would terraform leave transitive steps around? To my knowledge, Docker doesn't record a log the IO syscalls performed by a RUN directive, the layer just reflects the actual changes it makes. It uses overlayfs, doesn't it? If you create a temporary file and then delete it within the same layer, there's no trace that the temporary file ever existed in overlayfs, correct?
I'd get your worry if we were talking about splitting up a terraform config and running it across multiple RUN directives, but we're not, are we?
Transitive deps, not steps.
Random examples off the top of my head: Puppet has a ton of transitive Ruby libraries and config files/caches that it leaves around; Terraform leaves some very big provider caches on the system; plan or output files, if generated and not cleaned up, can contain secrets; even the “control group” of the status quo with RUN instructions often results in package manager indexes and caches being left in images.
Those are all technically user error (hence why I called them footguns rather than defects), but they add up and are easy mistakes to make.
Oof, not terraform please. If you use foreach and friends, dependency calculations are broken, because dependency happens before dynamic rules are processed.
I'd get much better results it I used something else to do the foreach and gave terraform only static rules.
Say more about this?
Do you mean that if you use a dynamic output in a foreach, Terrafom can error? Or are you referring to “dynamic” blocks and their interactions with iterators?
You can pretty much replace "docker build" with "go build".
But as long as people want to use scripting languages (like php, python etc) i guess docker is the neccessary evil.
>You can pretty much replace "docker build" with "go build".
I'll tell that to my CI runner, how easy is it for Go to download the Android SDK and to run Gradle? Can I also `go sonarqube` and `go run-my-pullrequest-verifications` ? Or are you also going to tell me that I can replace that with a shitty set of github actions ?
I'll also tell Microsoft they should update the C# definition to mark it down as a scripting language. And to actually give up on the whole language, why would they do anything when they could tell every developer to write if err != nil instead
Just because you have an extremely narrow view of the field doesn't mean it's the only thing that matters.
My point was that 90% of "dockerized" stuff is just scripting langs
Go is just one language, while Dockerfile gives you access to the whole universe with myriads of tools and options from early 1970s and up to the future. I don't know how you can compare or even "replace" Docker with Go; they belong to different categories.
In some situations, yes, others no. For instance if you want to control memory or cpu using a container makes sense (unless you want to use cgroups directly). Also if running Kubernetes a container is needed.
You have to differentiate container images, and "runtime" containers. You can have the former without the latter, and vice versa. They are entirely orthogonal things.
E.g. systemd exposes a lot of resource control as well as sandboxing options, to the point that I would argue that systemd services can be very similar to "traditional" runtime containers, without any image involved.
Well, I did mention "or use cgroups" above.
And what I've said is that there are more options. You don't have to use cgroups directly, there are other tools abstracting over them (e.g. systemd) that aren't also container runtimes.
Wasn’t this the same argument for .jar files?
> You can pretty much replace "docker build" with "go build".
Interesting. How does go build my python app?
It obviously means you dont use a scripting language, instead use a real langauge with a compiler.
Calling "go" a "real language" is stretching the definition quite a bit.
Real languages don't let errors go silently unnoticed.
For any serious app you dont ignore errors, and enforce them with the vast go linting tool.
Ok yeah let me just port pytorch over that should be quick
It doesn't sound like Golang is going to dominate and replace everything else, so Docker is there to stay.
At the risk of stating the obvious, there's quite a lot of languages besides just scripting languages and Go that get run in containers.
The math of “a decade” seemed wrong to me, since I remembered Docker debuting in 2013 at PyCon US Santa Clara.
Then I found an HN comment I wrote a few years ago that confirmed this:
“[...] I remember that day pretty clearly because in the same lightning talk session, Solomon Hykes introduced the Python community to docker, while still working on dotCloud. This is what I think might have been the earliest public and recorded tech talk on the subject:”
YouTube link: https://youtu.be/1vui-LupKJI?t=1579
Note: starts at t=1579, which is 26:19.
Just being pedantic though. That’s about 13 years ago. The lightning talk is fun as a bit of computing history.
(Edit: as I was digging through the paper, they do cite this YouTube presentation, or a copy of it anyway, in the footnotes. And they refer to a 2013 release. Perhaps there was a multi-year delay between the paper being submitted to ACM with this title and it being published. Again, just being pedantic!)
From another comment below, it's just a nice short title to convey that we're going back in time and not one to set your watch by.
We first submitted the article to the CACM a while ago.
The review process takes some time and "Twelve years of
Docker containers" didn't have quite the same vibe.
(The CACM reviewers helped improve our article quite a bit. The time spent there was worth it!)Makes sense! Thanks for working on it -- truly a wonderful paper!
You’re right, it was 2014. I was there on HN when docker was announced by shykes. It was a godsend because I was getting bummed by the alternatives like LXC, juju charms or vagrant.
Here’s the announcement from 2013:
Nice find. Check out shykes commenting here on that thread!
I still prefer LXC to docker. Improving libvirt and making virtualization a first-class OS feature with a library interface - vs. relying on an external tool and company interested in monetization - was and is the right approach.
That's why I love Incus. It offers all three so you don't have to choose. OCI app containers, LXC containers and KVM.
A full decade since we took the 'it works on my machine' excuse and turned it into the industry standard architecture ('then we'll just ship your machine to production').
(coauthor of the article here)
Well, before Docker I used to work on Xen and that possible future of massive block devices assembled using Vagrant and Packer has thankfully been avoided...
One thing that's hard to capture in the article -- but that permeated the early Dockercons -- is the (positive) disruption Docker had in how IT shops were run. Before that going to production was a giant effort, and 'shipping your filesystem' quickly was such a change in how people approached their work. We had so many people come up to us grateful that they could suddenly build services more quickly and get them into the hands of users without having to seek permission slips signed in triplicate.
We're seeing the another seismic cultural shift now with coding agents, but I think Docker had a similar impact back then, and it was a really fun community spirit. Less so today with the giant hyperscalars all dominating, sadly, but I'll keep my fond memories :-)
Great point about coding agents! Back then, Docker gave us 'it works on my machine, let's ship the machine'. Now, AI agents are giving us 'I have no idea how this works, let's ship the prompt'. The early Docker community spirit really was legendary though—before every hyperscaler wrapped it in 7 layers of proprietary managed services. Thanks for the memories and the write-up!
Thanks for the kind words! I've been prodding @justincormack to resurrect the single most fun OS unconference I've ever attended -- New Directions in Operating Systems (last held back in 2014). https://operatingsystems.io
Some of those talks strangely make more sense today (e.g. Rump Kernels or unikernels + coding agents seems like a really good combination, as the agent could search all the way through the kernel layers as well).
It seems that your entire profile is LLM generated comments, would appreciate it if you'd stop. Thanks.
I think you're too concerned with how others speak to believe that someone can actually write good, well-written comments. Obviously, in the age of LLMs, we might use one or two things to produce a better response. But that doesn't mean I don't think about what I wrote. Especially since English isn't my native language. Anyway, don't worry about me!
[dead]
>massive block devices assembled using Vagrant and Packer has thankfully been avoided...
Funny comment considering lightweight/micro-VMs built with tools like Packer are what some in the industry are moving towards.
And those lightweight VM base images are possible because Docker applied a downward pressure on OS base image sizes! Alpine Linux doesn't get enough credit for this; in addition to being a great base image, it was also the first distro to prioritise fast and small image creation (Gentoo and Arch were small, but not fast to create).
There's also the part where you have a much easier time building alpine packages inside a container rather than a VM. There is no docker run equivalent that lets you quickly run a shell script inside the deployment Linux distribution to build the package.
Maybe in that alternative future of massive block devices some downward pressure on image sizes would have been applied just the same.
It's not as easy; a block device has to be bootable and so usually bundles a kernel (large). And because the filesystem inside is opaque, you can't do layering like Docker does easily via overlayfs and friends. libguestfs does a heroic job of making VM images easier to manipulate from code, but it's an uphill battle...
Professor Madhavapeddy, am I understanding your comment correctly? "without having to seek permission slips signed in triplicate", the motivation to create Docker was because of IT bureaucracy?
I see this take a lot but I'd argue what Docker did was to entice everyone to capture their build into a repeatable process (via a Dockerfile).
"Ship your machine to production" isn't so bad when you have a ten-line script to recreate the machine at the push of a button.
Exactly my feeling. Docker is "works on this machine" with an executable recipe to build the machine and the application. Newer better solutions like OCI-compliant tools will gradually replace Docker, but the paradigm shift has provided a lot of lasting value.
Yeah docker codifies what the process to convert a base linux distro in to a working platform for the app actually is. Every company I've worked at that didn't use docker just has this tribal knowledge or an outdated wiki page on the steps you need to take to get something to work. Vs a dockerfile that exactly documents the process.
It's the ultimate in static linking. Perhaps a question that should be asked is why that approach is so compelling?
I question that as well, it's also why Go is extremely popular. Could it just be a pendulum swing back towards static linking?
Wonder when some enterprising OSS dev will rebrand dynamic linking in the future...
CGO_ENABLED=0 is sigma tier.
I don't care about glibc or compatibility with /etc/nsswitch.conf.
look at the hack rust does because it uses libc:
> pub unsafe fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V)
> I don't care about glibc or compatibility with /etc/nsswitch.conf.
So what do you do when you need to resolve system users? I sure hope you don't parse /etc/passwd, since plenty of users (me included) use other user databases (e.g. sssd or systemd-userdbd).
Most software doesn't need to resolve users. You also can always shell out to `id` if you need an occasional bit of metadata.
That's a fair point, and shelling out to id is probably a good solution.
I guess what bothers me is the software authors who don't think this through, leaving applications non-functional in these situations.
At least with Go, if you do CGO_ENABLED=0, and you use the stdlib functions to resolve user information, you end up with parsed /etc/passwd instead of shelling out to id. The Go stdlib should maybe shell out to id instead, but it doesn't. And it's understandable that software developers use the stdlib functions without thinking all too much about it. But in the end, simply advocating for CGO_ENABLED=0 results in software that is broken around the edges.
On the other hand, the NSS modules are broken beyond fixing. So promoting ecosystems that don't use them might finally spur the development of alternatives.
Could be interesting. What do you see as the main problems with NSS? I've never needed to use it directly myself. It seems quite crusty of course, but presumably there's more that your referencing.
Moving from linking stuff in-process to IPC (such as systemd-userdbd is promoting) _seems_ to me like a natural thing to do, given the nastiness that can happen when you bring something complex into your own address space (via C semantics nonetheless). But I'm not very knowledgeable here and would be interested to hear your overall take.
NSS/PAM modules have to work inside arbitrary environments. And vice versa, environments have to be ready for arbitrary NSS modules.
For example, you technically can't sandbox any app with NSS/PAM modules, because a module might want to send an email (yes, I saw that in real life) or use a USB device.
NSS/PAM need to be replaced with IPC-based solutions. systemd is evolving a replacement for PAM.
And for NSS modules in particular, we even have a standard solution: NSCD. It's even supported by musl libc, but for some reason nobody even _knows_ that it exists. Porting the NSCD protocol to Go is like 20 minutes of work. I looked at doing that more than once, but got discouraged by the other 99% of complexity in getting something like this into the core Go code.
the real trick was making "ship your machine" sound like best practice and ten years later we r doing the same thing with ai "it works in my notebook" jst became "containerize the notebook and call it a pipeline" the abstraction always wins because fixing the actual problem is just too hard.
> fixing the actual problem is just too hard.
I think it’s laziness, not difficulty. That’s not meant to be snide or glib: I think gaining expertise in how to package and deploy non-containerized applications isn’t difficult or unattainable for most engineers; rather, it’s tedious and specialized work to gain that expertise, and Docker allowed much of the field to skip doing it.
That’s not good or bad per se, but I do think it’s different from “pre-container deployment was hard”. Pre-container deployment was neglected and not widely recognized as a specialty that needed to be cultivated, so most shops sucked at it. That’s not the same as “hard”.
It's not even laziness or expertise. A lot of people are against learning conventions. They want their way, meaning what works on their computer. That's why they like the current scope of package managers, docker, flatpack,... They can do what they want in the sandbox provided however nonsensical and then ship the whole thing. And it will break if you look at it the wrong way.
I mean, walking through a door is easier than tearing down a wall, walking through it, and rebuilding the wall. That doesn't mean the latter is a good idea.
...while completely forgetting about security
>'then we'll just ship your machine production'
Minus the kernel of course. What is one to do for workloads requiring special kernel features or modules?
Those are global to the machine; generally not an issue and seccomp rules can filter out undesirable syscalls to other containers. But GPU kernel/userspace driver matching has been a huge headache; see https://cacm.acm.org/research/a-decade-of-docker-containers/... in the article for how the CDI is (sort of) helping standardise this.
I think it was all about efficient large-scale server machine reproducibility and not about making local development workflows easier. If it were i'm sure Docker would look much more friendly for that case but it still advanced to an industry-standard even for small software departments because everyone used it.
Oh, thank you... I'm not alone... I'm so tired of seeing crappy containers with pseudo service management handled by Dockerfiles, used instead of proper and serious packaging like that of many venerable Linux distributions.
In 2002 I used to think why cant they package a website. These .doc installation instructions are insane! What a waste of someones time.
I sort of had the problem in mind. Docker is the answer. Not clever enough to have inventer it.
If I did I would probably have invented octopus deploy as I was a Microsoft/.NET guy.
Linux user space is an abject disaster of a design. So so so bad. Docker should not need to exist. Running computer programs need not be so difficult.
Trying to convince people usually makes any resistance worse.
Using it, solving problems with it, and building a real community around it tend to make a much greater impact in the long run.
Yeah, but if the problem you are solving is rare for most practitioners, effectively theoretical until it actually happens, then people won't switch until they get bit by that particular problem.
But they’re roughly the same paradigm as docker, right? My understanding of the Nix approach is that it’s still reproducing most of a user land/filesystem in a captive/separate/sandbox environment. Like, docker is using namespaces for more stuff, Nix has a heavier emphasis on reproducibility/determinism, but … they’re both still throwing in the towel on deploying directly on the underlying OS’s userland (unless you go all the way to nixOS) and shipping what amounts to a filesystem in a box, no?
I daily drive NixOS. I don't have a global "userland". Packages are shipped from upstream and pull in the dependencies they need to function.
That means unlike Gentoo, I've never dealt with a "slot conflict" where two packages want conflicting dependencies. And unlike Ubuntu, I have new versions of everything.
Pick 2: share dependencies, be on the bleeding edge, or waste your time resolving conflicts.
Yeah nix is great for this. Also I can update infrequently and still package anything I want bleeding edge without any big issues other then maybe some build from sourcing.
> But they’re roughly the same paradigm as docker, right?
Absolutely not. Nix and Guix are package managers that (very simplified) model the build process of software as pure functions mapping dependencies and source code as inputs to a resulting build as their output. Docker is something entirely different.
> they’re both still throwing in the towel on deploying directly on the underlying OS’s userland
The existence of an underlying OS userland _is_ the disaster. You can't build a robust package management system on a shaky foundation, if nix or guix were to use anything from the host OS their packaging model would fundamentally break.
> unless you go all the way to nixOS
NixOS does not have a "traditional/standard/global" OS userland on which anything could be deployed (excluding /bin/sh for simplicity). A package installed with nix on NixOS is identical to the same package being installed on a non-NixOS system (modulo system architecture).
> shipping what amounts to a filesystem in a box
No. Docker ships a "filesystem in a box", i.e. an opaque blob, an image. Nix and Guix ship the package definitions from which they derive what they need to have populated in their respective stores, and either build those required packages or download pre-built ones from somewhere else, depending on configuration and availability.
With docker two independent images share nothing, except maybe some base layer, if they happen to use the same one. With nix or Guix, packages automatically share their dependencies iff it is the same dependency. The thing is: if one package depends on lib foo compiled with -O2 and the other one depends on lib foo compiled with -O3, then those are two different dependencies. This nuance is something that only the nix model started to capture at all.
> Docker ships a "filesystem in a box", i.e. an opaque blob, an image. Nix and Guix ship the package definitions from which they derive what they need to have populated in their respective stores, and either build those required packages or download pre-built ones from somewhere else, depending on configuration and availability.
The rest of your endorsement of NixOS is well taken, but this is a silly distinction to draw. Dockerfiles and nix package definitions are extremely similar. The fact that docker images are distributed with a heavier emphasis on opaque binary build step caching, and nix expressions have a heavier emphasis on code-level determinism/purity is accidental. The output of both is some form of a copy of a Linux user space “in a box” (via squashfs and namespaces for Docker, and via path hacks and symlinks for Nix). Zoom out even a little and they look extremely alike.
> This nuance is something that only the nix model started to capture at all.
Unpopular opinion, loosely held: the whole attempt to share any dependencies at all is the source of evil.
If you imagine the absolute worst case scenario that every program shipped all of its dependencies and nothing was shared then the end result would be… a few gigabytes of duplicated data? Which could plausible be deduped at the filesystem level rather than build or deployment layer?
Feels like a big waste of time. Maybe it mattered in the 70s. But that was a long, long time ago.
I think the storage optimization aspect is secondary, it is more about keeping control over your distribution. You need processes to replace all occurrences of xz with an uncompromised version when necessary. When all packages in the distribution link against one and the same that's easy.
Nix and guix sort of move this into the source layer. Within their respective distributions you would update the package definition of xz and all packages depending on it would be rebuild to use the new version.
Using shared dependencies is a mostly irrelevant detail that falls out of this in the end. Nix can dedupe at the filesystem layer too, e.g. to reduce duplication between different versions of the same packages.
You can of course ship all dependencies for all packages separately, but you have to have a solution for security updates.
Node.js basically tried this — every package gets its own copy of every dependency in node_modules. Worked great until you had 400MB of duplicated lodash copies and the memes started.
pnpm fixed it exactly the way you describe though: content-addressable store with hardlinks. Every package version exists once on disk, projects just link to it. So the "dedup at filesystem level" approach does work, it just took the ecosystem a decade of pain to get there.
nix has a cache too but only if the packages are reproducible.
Much harder to get reproducibility with C++ than JavaScript to say the least.
> If you imagine the absolute worst case scenario that every program shipped all of its dependencies and nothing was shared then the end result would be… a few gigabytes of duplicated data?
Honestly, I've seen projects that do this. In fact, a lot of projects that do this, at the compilation level.
It feels like a lot of the projects that I would want to use from git pull in their own dependencies via submodules when I compile them, even when I already have the development libraries needed to compile it. It's honestly kind of frustrating.
I mean, I get it - it makes it easier to compile for people who don't actually do things like that regularly. And yeah, I can see why that's a good thing. But at the very least, please give me an option to opt out and to use my own installed libraries.
Maybe the RAM crunch will get people optimizing for dedup again.
Windows is an order of magnitude better in this regard.
It used to be, but only in cases where your distro doesn't just package whatever software you require. Nowadays I prefer Flatpak or AppImage over crappy custom Windows installers for those cases. They allow for sandboxing and reliable updating/deinstallation.
These days, I equate anything that ships via docker/flatpak first as built by someone that only care about their own computer, especially if the project is opensource. As soon as a library or a tool update, they usually rush to add a hard condition on it for no reason other than to be on the "bleeding edge".
I'm with you on this, but I do want to point out that a big reason that people will update bundled libraries like that is because they don't want to put the effort in to see whether their bundled library versions actually have any critical vulnerabilities that affect the project. It's easier to update everything and be sure that there are no critical vulnerabilities.
In other words, the Microsoft Windows update process as applied to software development.
And yet I'm constantly getting asked when we'll support Windows containers at my office.
We've given up on native Windows containers in OCaml after trying to use them for our CI builds for many years. See https://www.tunbury.org/2026/02/19/obuilder-hcs/ for our recent switch to HCS instead. Compared to Linux containers, they're very much a second-class citizen in the Microsoft worldview of Docker.
This is because your team doesn’t know how to ship software without using containers.
If you have adopted a bad tool then people are likely to want the bad tool in more places. This is the opposite of a virtuous cycle and is a horrible form of tech debt.
Wow! That's digging deep in history. Has Plan9 been updated for modern hardware?
Windows.