KernelRanch
返回文章列表

Windows 上 docker run -p 绑不上端口:三种报错、一次不可复现的失败和一个反直觉结论

2026-11-10 #基础设施

在 Windows 上跑容器做数据库实验,最烦的不是数据库本身,是 -p 起不来。我这两天在同一台机器上遇到两种不同的失败:一种是一次性的 WSAEACCES,之后再也没复现;另一种是稳定可复现的"端口已被占用"。查资料时发现网上多数文章把前者归因于"Windows 保留了端口段",但我按那个思路做了实验,结论并不成立。

0. 先把两次真实报错摆出来

第一次,映射 55432(2026-11-09 23:04,Windows 11 + Docker Desktop 29.4.3):

Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:55432 -> 127.0.0.1:0:
listen tcp 0.0.0.0:55432: bind: An attempt was made to access a socket in a way forbidden
by its access permissions.

这是 Win32 的 WSAEACCES(错误码 10013),不是"端口被占用"。

第二次,映射 26005,同时用 .NET 在 Windows 侧监听同一端口(2026-11-10 13:19):

docker: Error response from daemon: failed to set up container networking:
driver failed programming external connectivity on endpoint ptest4 (...):
Bind for 0.0.0.0:26005 failed: port is already allocated

同样是失败,但根因完全不同:一个是权限/保留,一个是占用。先分清这两类,再谈怎么修。

1. 关键实验:同一个端口,隔一天就好了

时间端口操作结果
11-09 23:0455432docker run -p 55432:5432WSAEACCES,容器未启动
11-10 13:1655432同样的命令成功,0.0.0.0:55432->5432/tcp
11-10 13:1625432docker run -p 25432:5432成功
11-10 13:1926005Windows 侧已有监听失败,port is already allocated
11-10 13:1926005加 Windows 保留段 26000–26009 后成功
11-10 13:2026005删除保留段后成功

三个结论直接和网上的常见说法冲突:

  1. 55432 不是"被永久封的端口",隔天同一命令就能用。
  2. 保留段确实存在(下面列出来了),但它并不阻止 Docker Desktop 的端口映射。
  3. 能稳定复现的失败只有一种:端口已经被别的进程占住。

2. 为什么保留段挡不住 Docker

先看这台机器上真实存在的两样东西。

保留段(netsh int ipv4 show excludedportrange protocol=tcp,节选):

Start Port    End Port
      5357        5357
     13508       13508
     27339       27339
     28385       28385
     28390       28390
     50000       50059     *
     50101       50200
     50492       50591

动态端口段(netsh int ipv4 show dynamicport tcp):

Start Port      : 49152
Number of Ports : 16384

也就是说 49152–65535 是 Windows 的临时端口段,55432 正落在里面。

但端口映射的实际监听者不是 docker-proxy,而是 wslrelay:

LocalPort 25432  OwningProcess 46216  ProcessName wslrelay  LocalAddress ::1

Docker Desktop 在 Windows 上是"WSL2 里跑 dockerd + Windows 侧 relay 转发"。真正的 TCP 监听由 Windows 侧的 relay 完成,而它对这个端口的绑定行为与普通 bind() 并不完全一致——我加上 26000–26009 的保留段后,-p 26005 照样绑定成功。所以"照着老文章改注册表、禁 Hyper-V、重启 winnat"这套操作,至少在这台机器上解释不了问题。

那 55432 那次的 WSAEACCES 是怎么来的?最合理的解释是当时那个端口被 Windows 侧的某个临时保留覆盖了(Hyper-V/WSL 启动时会动态预留端口块),过一段时间释放。这类失败的特征就是:同一命令隔一会儿再跑就好了,且换端口也好了。

3. 四步排查(按这个顺序,不要跳)

# 1) 这个端口现在有没有人监听
Get-NetTCPConnection -LocalPort 26005 -State Listen -ErrorAction SilentlyContinue |
  Select-Object LocalAddress,LocalPort,OwningProcess
(Get-Process -Id (Get-NetTCPConnection -LocalPort 26005 -State Listen).OwningProcess).ProcessName

# 2) 端口是否落在保留段
netsh int ipv4 show excludedportrange protocol=tcp

# 3) 端口是否落在动态段(49152 起)
netsh int ipv4 show dynamicport tcp

# 4) 容器到底绑成了什么(排除"容器自己没起来"的干扰)
docker port <容器名>
docker inspect <容器名> --format '{{json .NetworkSettings.Ports}}'

第 1 步查到 wslrelay 以外的进程,基本就是它占着;查到 wslrelay 但仍连不上,问题在容器内部(比如数据库还没起来),不在端口。

4. 三种失败对应三种处置

报错判定处置
port is already allocated端口被占(本机进程或上一个容器的残留 endpoint)结束占用进程,或 docker rm -f 掉失败容器再试;仍报错则换端口
bind: ... access permissions(WSAEACCES)该端口此刻被 Windows 侧保留/覆盖换端口(别纠结原因);或改用 1024–49151 区间;重启 Docker Desktop 后重试
容器 Up 但连不上端口映射成功,服务没就绪docker exec 进容器验证服务,别在宿主机上排查网络

一个容易忽略的细节:失败的容器会留下网络 endpoint。docker run 报 port is already allocated 之后,即使占用进程已经退出,立刻重试仍可能失败——先 docker rm -f 掉那个失败的容器再试。

5. 其实你多半不需要映射端口

做数据库实验时,宿主机端口几乎没有必要:

# 容器内直接连
docker exec -it pg psql -U postgres -d postgres

# 两个容器互访:建一个用户网络,用容器名当主机名
docker network create labnet
docker run -d --name pg --network labnet -e POSTGRES_PASSWORD=postgres postgres:18
docker run --rm --network labnet postgres:18 psql -h pg -U postgres -c 'select 1'

需要从宿主机访问时,也可以只绑回环,避免暴露到局域网:

docker run -d -p 127.0.0.1:25432:5432 -e POSTGRES_PASSWORD=postgres postgres:18

顺带说明:Windows 上 --network host 不是 Linux 的那个语义,它不会让容器共享宿主机网络栈,别指望用它绕过端口映射。

6. 一个可以直接用的可用性预检

绑定之前先问一句"这个端口现在能不能绑":

function Test-PortBindable([int]$Port) {
  try {
    $l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, $Port)
    $l.Start(); $l.Stop(); "OK  $Port"
  } catch { "FAIL $Port  $($_.Exception.InnerException.Message)" }
}
25432, 55432, 26005 | ForEach-Object { Test-PortBindable $_ }

注意这个预检只验证"普通 bind 能否成功",而 Docker 的 relay 走的是另一条路径,所以 OK 不保证 Docker 一定能绑(第 2 节的实验就是反例)。它的价值在于快速排除"明显被占用"的情况。

小结

Windows 上的端口映射失败分两类,处置思路完全不同。占用型是确定的、可复现的,报 port is already allocated,先看谁在监听、再清掉失败容器的残留。保留型表现为 WSAEACCES,但我实测下来它并不可靠复现:同一个 55432 隔一天就能用,人为加保留段也挡不住 Docker 的映射。

所以遇到 WSAEACCES,不要在"为什么"上花太多时间。换一个 1024–49151 之间、不在保留段的端口,十秒就能继续干活;真要查,按第 3 节的四步走一遍,别去改注册表。

对做实验的人来说,更彻底的办法是压根不映射端口——容器内 docker exec、容器间走自定义网络,能省掉这一整类问题。

数据来源与检索时间

检索时间 2026-11-10。报错文本、端口清单、监听进程均来自本机实测;官方文档用于确认端口发布机制。

来源URL对应数据
Docker 官方文档《Port publishing and mapping》https://docs.docker.com/engine/network/port-publishing/-p 通过防火墙规则做 NAT/PAT;发布到 0.0.0.0 对局域网可见、绑定 127.0.0.1 仅本机可访问;--network host 语义
Microsoft Learn《默认动态端口范围》https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/default-dynamic-port-range-tcpip-changWindows 动态端口段默认 49152–65535(本机实测 netsh 显示同一区间,共 16384 个)

核验说明

  • 两次失败与三次成功的命令、报错原文、时间戳均来自本机执行记录;容器与保留段实验结束后已清理。
  • 保留段实验通过 UAC 提权执行 netsh int ipv4 add excludedportrange,窗口内用非提权 shell 跑 docker run -p,结果仍为成功;实验后已删除该保留段。
  • 没有做的事:没有测"重启 winnat"对 WSAEACCES 的影响(会短暂中断本机网络),也没有在非 Docker Desktop 的 Windows 容器环境(如 containerd、Podman)上复现。因此第 2 节对 WSAEACCES 的解释是推断,不是定论——它的价值在于说明"换端口比查根因更划算"。