Allocate a free host port from the range for a single container port#4988
Allocate a free host port from the range for a single container port#4988s3onghyun wants to merge 1 commit into
Conversation
haytok
left a comment
There was a problem hiding this comment.
Thanks for creating this PR!
When the container port is a single port and the host port is specified as a range, Moby uses that range as a pool and dynamically allocates a free host port from it.
- https://github.com/docker/cli/blob/master/cli/command/container/opts.go#L432
- https://github.com/docker/go-connections/blob/main/nat/nat.go#L208C3-L215C4
- https://github.com/moby/moby/blob/master/daemon/libnetwork/portallocator/portallocator.go#L245C2-L281C32
Details of Docker's behavior:
Details
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
> docker run -d -p 127.0.0.1:3000-3001:8080/tcp nginx
c1b08c490d3c80f9614c4dce6259f2699020c0c83c75eb7964913cbace9328eb
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c1b08c490d3c nginx "/docker-entrypoint.…" 4 seconds ago Up 4 seconds 80/tcp, 127.0.0.1:3000->8080/tcp nervous_feynman
> docker run -d -p 127.0.0.1:3000-3001:8080/tcp nginx
f927b67be40779c03ee7c4dd4fb15ba74de2aece81683ec7cd5c91311097423d
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f927b67be407 nginx "/docker-entrypoint.…" 1 second ago Up 1 second 80/tcp, 127.0.0.1:3001->8080/tcp thirsty_yalow
c1b08c490d3c nginx "/docker-entrypoint.…" 10 seconds ago Up 10 seconds 80/tcp, 127.0.0.1:3000->8080/tcp nervous_feynman
nerdctl does not implement dynamic allocation within an explicit host range
As you know, nerdctl currently does not allocate a free host port from the range in this case, so implementing that could be one option.
Therefore, IMO there might be a better approach than this PR.
WDYT?
ec1ab4c to
40d493f
Compare
|
Great point, @haytok — thank you for the references and the clear Docker walkthrough. You're right that the better fix is to implement the pool behavior rather than reject the spec. I've reworked the PR to do exactly that. When the container side is a single port and the host side is a range (e.g. A genuine mismatch (both sides are ranges of unequal length, e.g.
|
There was a problem hiding this comment.
Pull request overview
This PR modifies pkg/portutil.ParseFlagP to change how it handles port specs where the host side is a range but the container side is a single port (e.g. 3000-3001:8080). The stated goal in the PR description is to reject host/container range-length mismatches to avoid silently dropping ports, but the current diff instead implements Docker-style “range as a pool” behavior and updates the unit test to expect success.
Changes:
- Adds logic to treat
hostRange:singleContainerPortas a pool and select the first free host port. - Updates the range-length mismatch validation behavior and associated error path.
- Adds a
TestParseFlagPcase for127.0.0.1:3000-3001:8080/tcpexpecting a successful mapping.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| pkg/portutil/portutil.go | Adds host-range-as-pool selection logic and adjusts mismatch validation. |
| pkg/portutil/portutil_test.go | Adds a new ParseFlagP test case for host range with single container port. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@s3onghyun Could you check the comments from copilot? |
40d493f to
2f7e70f
Compare
|
Thanks @AkihiroSuda — went through all three Copilot comments: Comments 1 & 2 (mismatch "contradicts the PR intent"): these were written against the original PR description, which said to reject the spec. After @haytok's review I reworked it from reject → Docker-style pool allocation and updated the title/description to match, so that contradiction no longer applies. The logic still rejects a genuine mismatch — when both sides are ranges of unequal length (e.g. Comment 3 (environment-dependent "first free port" assertion): valid — fixed. I removed the table case that asserted an exact
|
|
@s3onghyun Could you check the comments from copilot? |
|
@AkihiroSuda went through the two current (non-outdated) Copilot comments:
|
haytok
left a comment
There was a problem hiding this comment.
NIT: Overall, the comments here look a bit redundant, so it's better to clean them up for future readers.
|
Thanks @haytok, addressed both:
|
|
Please squash the commits |
3b62792 to
48a0eaf
Compare
|
Squashed into a single commit. |
48a0eaf to
41afc00
Compare
|
Done — updated the doc comment to describe the deterministic logic (occupies the first port of the range and asserts the container port binds to |
|
@s3onghyun It looks like the comments haven't been updated.
|
41afc00 to
43e1974
Compare
|
Apologies @haytok — my previous amend didn't actually stage the file, so the force-push pushed an identical tree and the comment never changed. Fixed for real this time ( |
When the container side is a single port and the host side is a range (e.g. `-p 3000-3001:8080`), nerdctl now treats the range as a pool and binds the container port to the first free host port in it, using getUsedPorts to skip ports already in use, matching Docker's behavior. Previously the extra host ports were silently dropped. Genuine range/range mismatches of unequal length are still rejected. The host IP is validated and normalized once, and the pool test occupies the first port of a range and asserts the free successor is chosen, so it fails without this change (Linux-only; skipped in rootless). Signed-off-by: s3onghyun <s3onghyun@users.noreply.github.com>
43e1974 to
d5361fd
Compare
|
Thanks @AkihiroSuda — addressed all four in
Still a single squashed commit. |
|
Please click the |
|
Done — resolved all the review conversations (yours, @haytok's, and the Copilot ones). For the Copilot thread about On CI: the only red job is |
What
When the container side is a single port and the host side is a range (e.g.
-p 3000-3001:8080), Docker treats the range as a pool and binds the container port to a free host port from it. nerdctl previously collapsed this to the first host port, silently dropping the rest of the range.This PR implements the Docker-compatible behavior: pick the first free port in the host range (via the existing
getUsedPorts) and map the container port to it.Note
This supersedes the original approach in this PR (which rejected the spec). Per @haytok's review, implementing Docker's pool allocation is the better fix, so the PR was reworked accordingly. A genuine mismatch — both sides are ranges of unequal length, e.g.
3000-3001:8080-8082— is still rejected, since it has no valid mapping.Test
TestParseFlagPcovers the single-container-port pool case (binds to the first free port) and keeps the genuine-mismatch error case.go test ./pkg/portutil/passes.