Repository navigation
Fix stale mapped ports after restarting a container on Docker Desktop - #1599
Draft
cristianrgreco wants to merge 2 commits into
Draft
cristianrgreco wants to merge 2 commits into
cristianrgreco wants to merge 2 commits into
Conversation
Docker Desktop's API proxy reports a container's published ports from its own list of port forwards, which it updates after the stop, start or restart call has returned. Until then inspect returns the host port from the previous run, so restart() and a reuse start of a stopped container could hand out a port that stops working moments later. restart() now stops the container, waits until the TCP host ports it had mapped refuse connections, then starts it. stop() without removal waits the same way before resolving.
This was referenced Oct 13, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Docker Desktop,
restart()and starting a stopped reusable container can return a mapped port that stops working moments later. Tests then fail withECONNRESET,ECONNREFUSEDorother side closedstraight after the restart.Cause
Docker Desktop's API proxy rewrites
NetworkSettings.Portsusing its own list of port forwards, looked up by container and container port. Desktop updates that list after the stop, start or restart call has already returned. Until it catches up, inspect reports the forward from the previous run. The engine's own socket reports the right port throughout.Seen on Docker Desktop 4.82.0 (engine 29.6.1, macOS arm64), with the same container inspected through both sockets right after
restartreturned (output abridged):The old forward still points at the container's IP, which usually survives a restart, so the wait strategy passes on the old port before Desktop closes it. The window is about 100 ms on an idle Desktop and seconds when other containers are starting and being removed.
Fix
Nothing in the Docker API marks the stale answer. What a client can observe is that the old host port refuses connections from the moment Desktop drops the old forward. This PR waits on that:
restart()stops the container, waits until every TCP host port it had mapped refuses connections, then starts it. It no longer calls Docker's restart endpoint.stop({ remove: false })andwithAutoRemove(false)wait the same way beforestop()resolves. That covers a stop followed by a reuse start in the same process.On runtimes that release the port when the stop call returns, the wait is one refused connection per port.
Decision for the reviewer
Two shapes are possible:
Docker Desktop.I chose the first because CI runs on Linux, so a Desktop-only branch would never run there.
Relation to sending an empty
HostPortA separate change is being considered for a different Docker Desktop failure, where a port is never published at all. It sends
HostPort: ""instead of"0", so Desktop picks the host port itself. That change does not fix this problem, and this PR works with either value:restart()HostPort: "", without this PRHostPort: "", with this PRThrough the Docker API alone, with
HostPort: "", the first inspect afterrestartreturned still had the old host port in 7 of 24 restarts.Limits
restartevent forrestart().HostConfig.AutoRemoveis removed by the stop and cannot be started again. The library only sets that for Ryuk.restart()is called before Desktop has opened the forward for the current run, the wait passes at once. The default wait strategy rules this out because it connects to the host port. A log-only wait strategy does not. Not seen in 32 attempts.Other implementations
None of them handles this, and I found no Docker report for it.
GenericContainer.java#L589-L600.MappedPortcall and accepts any populated value:docker.go#L187-L191.DockerContainer.cs#L876-L893.Verification
All on Docker Desktop 4.82.0, macOS arm64. Not run on Linux.
npm run format,npm run lint,npm run knip,npm run check-compiles: clean.npx vitest runonwait-until-host-ports-released.test.ts,generic-container-restart.test.ts,generic-container-reuse.test.tsandgeneric-container.test.ts: 71 passed, 1 skipped.npx vitest run packages/modules/redis/src packages/modules/valkey/src: Valkey passed. Redis failed once inshould start redis-stack with custom env and keeping default argswithTimed out after 30000ms while waiting for container ports to be bound to the host, on a first start, and passed 12 of 12 on rerun. That is a different Docker Desktop problem (its log showsbind: address already in usefor that container's forward), in code this PR does not touch.Red-green
The stale port needs Docker Desktop to be busy, so no test in the suite is red every time without the change.
restart()stop({ remove: false })then a reusestart()should release the mapped port before stop resolves when the container is not removed, whole fileexpected true to be false)The first two rows come from a temporary probe that is not part of this PR: 6 containers, 4 rounds, 3 runs each way, while 4 workers created, started and force-removed other containers. It compared
getMappedPort(8080)with the engine's own socket.The unit tests in
wait-until-host-ports-released.test.tsare new with the helper. Breaking the wait condition fails 3 of them, and breaking the UDP filter fails 1.Not breaking
No public API changes.
restart()still resolves once the container is ready again, andstop()still resolves with the stopped container.ContainerRuntimeClient'scontainer.restartis unchanged.