Docker Compose Just Quietly Became a Tiny Kubernetes — Here’s What v5.2 Actually Changed
Somewhere in the last two weeks, `docker compose up` started behaving slightly differently on your machine, and there was a decent chance you didn’t notice, because the changelog entry for it is one sentence long. On June 23, 2026, Docker Compose v5.2.0 shipped with a note that says, in full: a new reconciliation algorithm between the observed state and the expected state. That’s it. That’s the entire public explanation for what is, underneath, a genuine architectural rewrite of how Compose decides what to create, recreate, or tear down.
TL;DR
- Docker Compose v5.2.0 (June 23, 2026) replaced its old ad-hoc “convergence” logic with a reconciliation engine: an explicit snapshot of what’s actually running (
ObservedState) diffed against your compose file to produce a deterministic operation plan - This is the same architectural pattern Kubernetes controllers have used for years — compare observed vs desired state, compute a diff, execute the diff as a graph of operations, not as a pile of imperative if-statements
- The rewrite fixes three real bugs: a volume-divergence confirmation prompt that used to ask you twice, a container that could get stopped twice when both its network and its config changed, and a scale-down that didn’t correctly wait for cleanup before dependent services started
- The plan is now a proper dependency graph (DAG), executed with real concurrency where operations don’t depend on each other, instead of a sequential convergence loop
- Nothing in your compose file needs to change — this is entirely internal — but if you’ve been fighting inconsistent recreate behavior, this release is probably why it stops
Why docker compose up Needed More Than a Patch
For most of Compose’s life, deciding what to do on up was handled by something internally called “convergence” — a body of logic that inspected what existed, compared it to what the compose file wanted, and issued the necessary API calls more or less as it went. It worked, most of the time, for most projects. But ad-hoc convergence logic has a specific failure shape: every new edge case gets patched in wherever the code happens to be looking at that moment, and the interactions between patches compound in ways nobody fully models.
The Old Way: Convergence and Its Rough Edges
Three concrete bugs surfaced from exactly this shape of problem, and all three got fixed as part of the same rewrite. A volume whose configuration had drifted from the compose file triggered a confirmation prompt in one code path — and then, later in execution, a second code path noticed the same drift and asked again, so declining the first prompt didn’t actually stop the second one from appearing. A container whose network assignment and whose general config had both changed in the same run could get an OpStopContainer queued twice, once from the network-recreation path and again from the general recreation path, because neither path knew the other had already handled it. And scaling a service down didn’t reliably propagate a “wait for cleanup” edge to dependent services, so a dependent could start before the scale-down had actually finished removing excess containers.
You're Picking the Wrong AI Coding Tool — Here's What Actually Works in 2026 Most beginners pick an AI coding assistant the same way they pick a laptop — by looking at the price tag...
None of these are exotic bugs. They’re the natural result of convergence logic growing multiple independent code paths that each reason about “what needs to happen” without a shared, explicit model of what’s already true.
Observed State vs Desired State: The New Mental Model
The fix is conceptually simple, even though implementing it took a genuine rewrite: introduce an explicit ObservedState type that snapshots what Docker actually reports right now — running containers, existing networks, existing volumes — separately from the desired state your compose file describes. Reconciliation becomes a pure function: compare the two, and produce a plan. Nothing acts on anything while the comparison happens; the plan is data, computed once, before any API call fires.
What Counts as “Diverged,” and Why That Question Used to Have Multiple Answers
Under the old convergence model, “has this container diverged from the compose file” could get answered by more than one code path, sometimes inconsistently, which is exactly how the double-stop and double-prompt bugs happened. Under the reconciler, divergence detection lives in one place, and every downstream decision — recreate, leave alone, stop first, rename — reads from that single answer instead of re-deriving it.
The Plan: Turning a Diff Into a Dependency Graph
Here’s the part that actually matters for behavior you’ll notice. The reconciler doesn’t just produce a list of things to do — it produces a graph. Each operation (create a network, create a volume, create a container, stop a container, remove a container, disconnect a network) is a node, and nodes carry explicit dependency edges on each other.
// Conceptual shape of what the reconciler produces —
// not literal output, but this is the mental model
Plan{
CreateNetwork(app_net)
CreateVolume(db_data)
CreateContainer(db) // depends_on: [CreateVolume(db_data)]
CreateContainer(api) // depends_on: [CreateContainer(db), CreateNetwork(app_net)]
StopContainer(old_worker)
RemoveContainer(old_worker) // depends_on: [StopContainer(old_worker)]
CreateContainer(worker) // depends_on: [RemoveContainer(old_worker)]
}
A dedicated executor then walks this graph, running independent branches concurrently and respecting the dependency edges for everything else. This is the direct fix for the scale-down bug: the removal of excess containers is now a real node in the graph, and any dependent service’s create operation carries an explicit edge onto that removal, so it structurally cannot start early. The old bug wasn’t a missing check — it was a missing edge in a graph that didn’t fully exist yet.
Full Stack Development Building your first full stack project can feel overwhelming. Many beginners start coding without understanding full stack development, which often leads to broken environments, integration issues, and lost motivation. In this guide,...
Why the Fix for “Stopped Twice” Is a Graph Problem, Not an If-Statement Problem
The double-stop bug is a clean illustration of why this rewrite needed a graph and not just another conditional. When both a container’s network and its general configuration diverge, two different parts of the old logic each independently decided the container needed stopping, and neither checked whether the other had already done it. In the reconciler, every stop operation is a node with an identity. A second code path that wants to stop the same container reuses the existing stop node and chains its own work onto it, instead of creating a redundant second node. That’s not a bug fix bolted onto the old logic — it’s a property that falls out naturally from modeling operations as graph nodes with identity in the first place.
What Actually Changes for You in Practice
Your compose files don’t need to change. The CLI surface is identical. What changes is behavioral consistency: if you were seeing a container recreate for reasons you couldn’t quite explain, or a scale-down that seemed to race with a dependent service starting up, or that duplicate volume prompt, v5.2.0 is very likely why those stop happening. If your workflow scripts or CI pipelines were quietly working around any of these quirks — retrying on the double-prompt, adding a manual sleep after scaling down — those workarounds are now dead weight, not protection.
Should You Expect Anything to Break?
The release notes carry an explicit warning: if a previously-working Compose workload behaves differently after upgrading, the maintainers want to know about it, because the reconciler is new enough that edge cases in unusual compose configurations (heavy use of network_mode: service:x, complex depends_on chains, external providers) are the most likely place for a behavioral gap to still be hiding. If you run anything non-standard, treat the upgrade the way you’d treat any internal-engine rewrite: test in a non-critical environment before you rely on it in CI.
The Bigger Pattern: Why “Reconcile, Don’t Mutate” Keeps Winning
This is the same lesson Kubernetes learned years ago and that shows up all over resilient system design: don’t scatter “what needs to change” logic across a dozen code paths that each mutate the world as they go. Take one snapshot of what’s real, one description of what’s wanted, diff them in exactly one place, and only then act — as a plan you can log, inspect, and reason about before a single side effect happens. Compose’s convergence logic was the imperative version of this problem; the reconciler is the declarative fix, and it’s a pattern worth recognizing the next time you’re debugging a service that seems to make the same mistake twice depending on which code path got there first. If your own system has a “figure out what to do” function that’s grown three special cases for three near-identical bugs, that’s usually the same tell.
FAQ: Docker Compose’s New Reconciliation Engine
What changed in Docker Compose v5.2.0?
Compose replaced its internal “convergence” logic — the code that decided what to create, recreate, or remove on up — with a reconciliation engine that explicitly snapshots observed state, diffs it against the desired state from your compose file, and executes the result as a dependency graph of operations.
Why Kotlin AI Integration Keeps Blowing Up at Runtime — Real Errors, Real Fixes You've wired up the AI client, the first response comes back clean, and then — NullPointerException. Not in your code. In...
Do I need to change my docker-compose.yml file for v5.2.0?
No. This is an entirely internal change to how Compose decides what to do. Your compose file syntax and the CLI commands you run are unaffected.
Why was my container getting recreated for no obvious reason before this update?
Inconsistent divergence detection across multiple independent code paths in the old convergence logic was a common cause. The reconciler centralizes that detection into one place, which is one of the specific bugs this rewrite fixes.
What is the difference between observed state and desired state in Docker Compose?
Observed state is a snapshot of what Docker actually reports as currently running — containers, networks, volumes. Desired state is what your compose file describes. The reconciler’s entire job is comparing these two and producing a plan to close the gap.
Is this the same pattern Kubernetes uses?
Conceptually, yes. Kubernetes controllers have used observed-vs-desired-state reconciliation loops for years. Compose’s v5.2.0 rewrite applies the same architectural idea at a much smaller scale, for a single-host, single-run tool rather than a distributed control plane.
Does the new reconciler run operations in parallel?
Yes, where the dependency graph allows it. Operations without a dependency relationship execute concurrently through a dedicated executor, while operations with real dependencies — like removing a container before recreating it — are chained in the correct order.
Will upgrading to Compose v5.2.0 break my existing setup?
Most standard compose files should behave identically or better. The release notes flag this as a significant internal change and ask users to report any workload that previously worked but behaves differently, so testing in a non-critical environment before relying on it in CI is a reasonable precaution.
Where can I read the actual implementation details?
The change shipped through pull request #13830 in the docker/compose repository on GitHub, titled “Reconciliation plan.” As of this writing there’s no official blog post or documentation page dedicated to explaining the internals beyond the one-line release note.
Written by: