Docker Desktop Became a Line Item

Docker's grace period ran out three weeks ago and the argument everybody had was about $5 per user per month. This is about 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 on the thirty-first of August last year. Docker Desktop stays free for personal use, education, non-commercial open source and small businesses, and needs a paid subscription in any organization with more than 250 employees or more than $10 million in annual revenue. The published tiers were $5 per user per month for Pro, $7 for Team and $21 for the new Business tier, with a grace period that ran to the thirty-first of January.

The discussion that followed was about whether the price was fair, and I find 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.

The interesting part happened in the meetings afterward. 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 reached 1.0 last month. SUSE's desktop application hit 1.0.0 on the twenty-sixth of January, three weeks after the deadline it was answering. It runs on macOS, Windows and Linux, bundles a Kubernetes distribution, and by that release had dropped its own image-building tool in favor of the standard nerdctl and docker command line tools, which tells you exactly which compatibility surface it decided mattered.

Podman 4.0 arrived this month. 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 landed in the middle of February with a rewritten network stack, a WSL backend for podman machine on Windows, and the API socket mounted through to the host automatically. That last item is the whole migration story in one line, and it is a Docker-compatible API.

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. The business tier list price was $21 per user per month, so $252 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 in the first quarter and the gap widens after that. The arithmetic is uncomfortable and it is 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. 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 does not appear anywhere 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 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.

I want to be fair to the company in the middle of this, because the tone in January was not. 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.

What stays with me 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.