Docker Desktop Became a Line Item

Docker Desktop Became a Line Item

Two years after Docker's grace period ran out, the argument everybody had was about five dollars a user and the thing that actually decided who switched was not the price. This is what having to justify that line made teams say out loud: on a Mac or a Windows laptop the container runs inside a Linux virtual machine somebody has to maintain, and the file sharing layer between your source tree and that machine is the actual product.

The Invoice Arrived

Docker announced the change at the end of August 2021. Docker Desktop stayed free for personal use, education, non-commercial open source and small businesses, and needed a paid subscription in any organization above a threshold of employees or revenue. The tiers as published then ran from a few dollars a user a month up to about twenty for the business tier, and the grace period ran out at the end of that January.

The discussion that followed was about whether the price was fair, and I found that argument boring in both directions, because both positions are defensible and neither is an engineering question. A company that maintains software has to be paid for maintaining it, and a team that budgeted nothing is entitled to be annoyed when the bill arrives. Both are true at once and usually are.

Two years on it is possible to say something the argument at the time could not. A great many teams did leave, the alternatives they left for are now mature products rather than promising ones, and a noticeable number came back or ended up running both. What separated the two groups had almost nothing to do with the monthly rate. It was the shape of what their laptops do all day, and specifically how much of it crosses a filesystem boundary.

The interesting part happened in the meetings after the announcement. Somebody has to justify a recurring per-seat cost, and writing that justification means saying what the thing does. A remarkable number of people who had been running docker run daily for five years could not answer, and the answer they arrived at surprised them.

Because the honest answer is that on a Mac or a Windows laptop, the container is not running on your computer. It is running on a Linux machine that exists inside your computer, and almost everything the application does is make that machine feel like it is not there. The subscription did not create that arrangement. It just put a number next to it, and a number is what makes people look.

What the Application Does

It runs a Linux kernel, because that is what a container is. Containers are not a technology so much as a set of Linux kernel features used together: namespaces for isolation, control groups for limits, a union filesystem for layers. macOS has none of them and Windows has its own unrelated container types, so on either host there is a Linux kernel running in a virtual machine. On Windows that is usually the one WSL 2 provides. On a Mac it is one the application starts for you.

It ships a hypervisor that had to be rebuilt underneath everyone. When the Mac moved to a different processor architecture, the whole stack had to be rebuilt: a virtualization layer for the new chip, a Linux kernel built for it, and a story for the enormous number of published images compiled for the old one. That work took a long time, it happened while people carried on using the product, and nobody outside the team that did it thought about it once. It is the clearest thing in the subscription.

It maintains a filesystem bridge that has to lie convincingly. Your source tree lives on the host and the container needs it at a path inside the guest. Docker's documentation lists which host directories are shareable by default and warns that because macOS filesystems are case-insensitive and Linux is not, shared files are made case-sensitive: create test and open Test and you get no such file. That is a semantics translation dressed as a mount, and it is where the difficulty lives.

It exposes a socket that pretends the daemon is local. Everything that speaks Docker expects a socket on the host, and behind that socket sits a daemon in the guest. Compose talks to it. Test harnesses that start containers talk to it. Editor integrations talk to it, and so does any container you started with the socket bind-mounted in so it could start siblings. That one file path is the compatibility surface of the entire ecosystem, and every replacement is judged on how faithfully it reproduces it.

Across the Boundary

On a Linux workstation, mounting your working directory into a container costs approximately nothing. It is a bind mount: the same kernel, the same inodes, the same page cache, one namespace looking at a directory another namespace also has. No copy, no protocol. The command is cheap to type because on the machine it was designed for it is also cheap to run.

Across a virtual machine boundary none of that holds. Two kernels, two page caches, no shared inodes, and every open, stat, read and write becomes a message on a channel between them. The cost per operation is small and the number of operations is not. A dependency installer or a build tool walking a project tree issues a huge volume of metadata calls, most of which exist only to discover that a file is absent, and each one now pays the crossing.

Change notification is worse, because it has no correct implementation. A framework's watch mode wants to be told by the kernel when a file changed, and the kernel that saw the change is not the kernel the process is asking. The options are to forward events across the boundary and accept that some are lost or late, or to poll the whole tree on a timer and burn a processor core for the privilege. Both of those are things people actually run.

The tell is in the tuning knobs. Docker exposed mount consistency options so you could declare that your program did not need the host and container views of a file to agree at every instant, which is a correctness setting presented as a performance setting. Microsoft is blunter: its own comparison of WSL versions marks performance across the operating system filesystems as the single thing WSL 2 does worse than WSL 1, and recommends keeping project files on the same side as the tools that read them.

What You Can Run Instead

Rancher Desktop arrived within weeks of the deadline. SUSE's desktop application reached 1.0 in January 2022, three weeks after the date it was answering, and has shipped steadily since. It runs on macOS, Windows and Linux, bundles a Kubernetes distribution, and by that first release had already dropped its own image-building tool in favor of the standard nerdctl and docker command line tools, which told you exactly which compatibility surface it had decided mattered.

Podman went from a command line tool to a desktop product. Podman's design is genuinely different rather than a repackaging: no long-lived daemon, rootless by default, and containers as ordinary child processes. Version 4.0 in February 2022 brought a rewritten network stack, a WSL backend for podman machine on Windows, and the API socket mounted through to the host automatically, which is the whole migration story in one line because it is a Docker-compatible API. Its own desktop application reached 1.0 the following year, which is the part that mattered for anyone who was never going to adopt a tool without a window.

Or you run a virtual machine yourself. Point DOCKER_HOST at a daemon over SSH, on a virtual machine you manage or a spare Linux box under a desk, and the client on your laptop talks to it as if it were local. You lose the file sharing entirely, which is either the point or the problem depending on whether your workflow depends on editing files the container can see. Nothing about this is new and it works today.

And there is the option that dissolves the question. On a Linux workstation none of this apparatus exists. No hypervisor, no guest kernel, no file sharing protocol, no socket proxy, and a bind mount that costs what a bind mount costs. Every product in this category is a compatibility shim for developing on an operating system that cannot run the artifact you are building, which is worth saying once even though it is not advice most teams can act on.

What the Move Actually Costs

Bind mount throughput is the first thing anyone notices and the hardest to compare, because each option implements sharing differently and the gap between them depends on the shape of your tree rather than its size. A benchmark that copies a large file tells you nothing useful. The measurement worth taking is the operation your team performs forty times a day, run on your actual repository, on each candidate, before anyone commits to a plan.

Socket compatibility is the sharper cost, because it does not degrade, it fails. Anything that mounts the Docker socket into a container to control the daemon from inside it, test libraries that spin up a database container per suite, editors that attach to running containers, tools that shell out to the command line and parse what comes back. Each of those is a small assumption about an API surface, and each one is found by tripping over it. Rootless defaults add a second set: file ownership on shared directories, low port binding, and any image that assumes it is a real root.

Then the cost nobody puts in the comparison. After the migration, the virtual machine on every laptop is your organization's to understand. When it will not start, when the disk image has grown to fill the volume, when a new hire's machine behaves differently from everyone else's, the person who works it out is on your payroll. That labor did not disappear when the invoice did. It moved from a vendor's support queue onto somebody's Thursday.

The Case for Simply Paying

Weigh the arithmetic against an hour of engineering time. Take the business tier as it was priced then, around twenty dollars a user a month, and it comes to a couple of hundred a year per person. Put any plausible hourly rate for an engineer against that and the annual cost is a few hours. If the migration takes each developer half a day and the replacement costs an hour a month in friction, paying was cheaper inside the first quarter and the gap widened after that. The arithmetic is uncomfortable and it was right for a lot of teams.

Somebody did the hard part already. The processor architecture transition is the example, but the ongoing version is duller and matters more: a guest kernel that gets patched, a hypervisor that keeps working through host operating system updates, an installer that behaves on a machine you have never seen, and a support channel that is not your colleague. Free-of-charge software with a commercial maintainer is being paid for by somebody, and the question was only ever whether it would be you.

The tool became a procurement item, which is its own cost. The reason a lot of teams left had nothing to do with the price, and two years of watching it has not changed my mind. A per-seat license is a purchase order, an approval, a renewal and a seat count somebody has to keep accurate, and in plenty of organizations that machinery takes weeks. A new engineer who cannot build anything until a license is assigned is more expensive than either option, and that failure mode appears nowhere in a comparison of monthly rates.

So the axis is not price against free. Framed as cost, the decision is trivial and mostly favors paying. Framed as who is accountable when the virtual machine on a laptop stops working on a release morning, it is a real decision with a real answer that differs by team. One of those framings produces a spreadsheet and the other produces a plan, and that January was spent almost entirely on the first one.

Two Questions, Neither About Money

The first is what your inner loop actually touches. If developers start a few containers in the morning, leave them running and talk to them over a port, the file sharing layer is nearly irrelevant and any of these options will do; pick on familiarity and stop thinking about it. If the loop is a large source tree that a build tool walks on every save, the sharing layer is the product you are selecting and the decision should be made with a stopwatch on your own repository rather than with an opinion.

The second is what happens to a person who joins on Monday. Whatever you choose has to be installable, licensed, documented and working before lunch on their first day, on a laptop model nobody has tested. That constraint eliminates more options than performance does, and it is the one that gets discovered late, usually by the new person, who concludes something about the organization that is difficult to unlearn.

The tone at the time was not fair to the company in the middle of it. Docker built the thing that made containers usable by people who were never going to configure a Linux virtual machine themselves, and then asked large organizations for a list price that is a rounding error next to the salaries of the people using it. The complaint was never really about the amount. It was about being surprised, which is what happens when nobody has looked at a dependency in five years.

The part that lasts is smaller than the licensing question. The container that started on my laptop this morning is running on a Linux machine I have never logged into, on hardware with no Linux anywhere in it, and the layer making my files visible to that machine is doing more work than everything running on top of it. That was true the whole time and it cost nothing to ignore. Finding out what a tool charges is usually the first time anyone asks what it was doing.