This is the kind of error that wastes a whole afternoon if you do not know what you are looking at. You run docker-compose build from a script that is supposed to build three containers. The build itself looks fine. Then docker-compose up -d fails with a name conflict and you see a container you did not ask for, named moby/buildkit:buildx-stable-1, sitting right next to the proxy, ui, and backend services you did ask for. The natural reaction is to chase the wrong problem. You rename your services. You change the container_name fields. You start adding container_name: lines to every service and that fixes it for about ten seconds, then the conflict comes back when you touch one more service. You have not actually fixed anything. You have just described the symptom in a different way.
The actual cause is that docker-compose build does not run the local Docker engine’s plain build path on a modern Docker Desktop install. It calls Buildx, the upstream build subsystem, which spins up its own sidecar container called a BuildKit builder to do the actual compilation, image tagging, and layer caching. On Docker Desktop for macOS that builder container uses the image docker.io/moby/buildkit:buildx-stable-1, runs in a Docker context (a named endpoint that points at a Docker daemon, local or remote) named default, and gets the container name moby/buildkit:buildx-stable-1. None of this is a misconfiguration on your part. It is the documented behaviour. The reason you have not seen it before is that the BuildKit builder runs and exits inside the same Compose invocation, so for most people its container disappears by the time docker-compose up -d is invoked. The visible name conflict only happens when something about the runtime or your previous build leaves the BuildKit container hanging around.
Why the container sticks around
There are three real reasons for the sticky BuildKit container, and the symptom looks the same for all three. The first is a previous docker-compose build invocation that was killed or interrupted. Compose tries to clean up the BuildKit container when build finishes, but if the parent shell lost its connection to the daemon mid-build, the cleanup hook did not run and the container stays in the exited state. The second reason is a hand-rolled Docker context named default that points at a remote Docker daemon. Buildx picks up the default context, spawns the BuildKit container inside the remote daemon, and the container lives where your local docker ps cannot see it. From docker-compose up‘s point of view, the name moby/buildkit:buildx-stable-1 is already taken (by the remote BuildKit), so any service you name that gets a name collision. The third reason is a custom builder pinned to a fixed name. If you wrote a docker-container://my-fixed-builder driver in your docker-compose.yaml or created one with docker buildx create --name my-fixed-builder --driver docker-container, every build will spawn a BuildKit container named moby/buildkit:<that-name>-1 instead of the generic stable name. If you named the builder something that overlaps with a container_name field in your compose file, the conflict is waiting to happen.
The thread on Server Fault that surfaced this question has the canonical recipe for diagnosing which case you are in. Run docker context ls and look at the asterisk. If it does not point at your local daemon, you are in case two and the fix is to switch back to the local context with docker context use default. Run docker ps -a --filter name=buildkit and look at the existing container. If it is in the exited state and has been there for more than a few minutes, you are in case one and the fix is docker rm on the orphan. If the existing container has a name that mentions a builder you created, you are in case three and the fix is to delete the builder and let Buildx fall back to its default.
A short checklist for the diagnostic step:
- Run
docker context lsand read the asterisk at the start of the row. If it does not point at the local daemon, your Buildx is hitting a remote context. - Run
docker ps -a --filter name=buildkit. If a BuildKit container is in theexitedstate and has been there longer than the build you just ran, it is an orphan from a previous interrupted session. - Run
docker buildx ls. If there is a builder listed that you did not create, or that has a name matching acontainer_namein your compose file, it is the source of the conflict. - If all three of the above return empty, you are seeing the sidecar for the first time and the conflict is coming from a race condition between Buildx and
docker-compose up. Rundocker-compose down --remove-orphansto clear stale compose state and try the build again.
How Buildx decides what to spawn
The spawning is not magic and it is configurable. Buildx keeps a registry of builders, each with a driver (docker, docker-container, remote, or kubernetes). The driver that runs the BuildKit container in a sidecar is docker-container, and every builder created with that driver will spawn the moby/buildkit: sidecar at build time. The driver that runs BuildKit as a child process of the Docker daemon is docker, and that one does not spawn a sidecar at all. Buildx picks the default builder based on your DOCKER_BUILDKIT environment variable and your ~/.docker/buildx/cache/config.json file. If neither is set, recent Docker Desktop defaults to the docker-container driver because the experience is more consistent across macOS, Windows, and Linux. That default is also why most people hit this without ever reading the docs.
The simple fix for case two is one command:
docker context use default
docker buildx use default
That tells both Compose and Buildx to look at the local daemon, and the sidecar that gets spawned will live and die with the build. You do not get a persistent named container, you do not get a remote BuildKit held hostage by a stale connection, and your existing container_name fields in compose will not collide with anything Buildx touches.
For case three the fix is similar in shape but different in target:
docker buildx ls # show builders
docker buildx rm my-fixed-builder
docker buildx use default
Remove the custom builder. Tell Buildx to use default for everything. From this point on, the BuildKit sidecar will use the standard moby/buildkit:buildx-stable-1 name, and it will be cleaned up by Compose when the build ends.
For case one (the orphan container), there is a one-liner you can run before every build, but you do not actually need it once you switch to the local context:
docker rm -f $(docker ps -aq --filter name=buildkit) 2>/dev/null
That clears any leftover BuildKit containers without affecting anything else.
Trade-offs
The trade-off is reproducibility. The docker-container driver persists the BuildKit container, which means it persists the layer cache (the directory where Docker stores previously-built image layers so they can be reused across builds) and the build history across builds. If you are doing something that benefits from a persistent cache (multi-stage Dockerfiles, large compiled dependencies, hermetic builds) the custom builder pays for itself. The local daemon driver gets the BuildKit cache cleared every time the daemon restarts on macOS, so your second build of the morning is going to be a cold rebuild. Most small Compose files do not notice. Big Dockerfiles with three or four compiled stages will.
The other trade-off is that the docker context use default fix only helps when the local context actually is what you want. If you were using a remote daemon by design (you run Docker on a Linux box somewhere and have macOS as the client), this fix reroutes everything to the local daemon, including docker-compose up, which then fails to find the containers you actually meant to run. In that case the right fix is to keep the remote context but configure the Buildx builders explicitly so they do not conflict with your container_name fields. That is more setup work and is one of the reasons most teams pick a single daemon rather than juggling contexts.
Bottom line
The moby/buildkit:buildx-stable-1 container is Buildx’s sidecar, not your service, and it is not a bug in your compose file. If it is sticking around between builds and conflicting with container_name fields, you are looking at one of three causes: an orphan container from a previous interrupted build, a remote default context pulling a remote BuildKit, or a custom builder you (or someone on your team) created at some point and never deleted. The first two get fixed by docker rm plus docker context use default. The third gets fixed by docker buildx rm <name>. None of these are heavy fixes. They all run in under five seconds. The hard part is knowing which one to run, and that is what docker context ls plus docker ps -a --filter name=buildkit is for.