Windows 上 docker run -p 绑不上端口:三种报错、一次不可复现的失败和一个反直觉结论
在 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:04 | 55432 | docker run -p 55432:5432 | WSAEACCES,容器未启动 |
| 11-10 13:16 | 55432 | 同样的命令 | 成功,0.0.0.0:55432->5432/tcp |
| 11-10 13:16 | 25432 | docker run -p 25432:5432 | 成功 |
| 11-10 13:19 | 26005 | Windows 侧已有监听 | 失败,port is already allocated |
| 11-10 13:19 | 26005 | 加 Windows 保留段 26000–26009 后 | 成功 |
| 11-10 13:20 | 26005 | 删除保留段后 | 成功 |
三个结论直接和网上的常见说法冲突:
- 55432 不是"被永久封的端口",隔天同一命令就能用。
- 保留段确实存在(下面列出来了),但它并不阻止 Docker Desktop 的端口映射。
- 能稳定复现的失败只有一种:端口已经被别的进程占住。
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-chang | Windows 动态端口段默认 49152–65535(本机实测 netsh 显示同一区间,共 16384 个) |
核验说明
- 两次失败与三次成功的命令、报错原文、时间戳均来自本机执行记录;容器与保留段实验结束后已清理。
- 保留段实验通过 UAC 提权执行
netsh int ipv4 add excludedportrange,窗口内用非提权 shell 跑docker run -p,结果仍为成功;实验后已删除该保留段。 - 没有做的事:没有测"重启 winnat"对 WSAEACCES 的影响(会短暂中断本机网络),也没有在非 Docker Desktop 的 Windows 容器环境(如 containerd、Podman)上复现。因此第 2 节对 WSAEACCES 的解释是推断,不是定论——它的价值在于说明"换端口比查根因更划算"。