别让容器端口“裸奔”:docker compose 端口收敛与 Nginx 单入口实践

举报
辣椒酱来了 发表于 2026/11/12 13:39:17 2026/11/12
【摘要】 一个很常见的开头上周帮朋友整理一台内网服务器的部署脚本,docker compose ps 一看,端口列表是这样:0.0.0.0:5173->5173/tcp0.0.0.0:19000->19000/tcp0.0.0.0:19200->19200/tcp脚本能跑,功能也正常,但它意味着:这三个端口对所有能访问到这台机器的人都敞开着。为了让它能用,安全组被迫开了三个洞,而洞的源地址是 0.0...

一个很常见的开头

上周帮朋友整理一台内网服务器的部署脚本,docker compose ps 一看,端口列表是这样:

0.0.0.0:5173->5173/tcp
0.0.0.0:19000->19000/tcp
0.0.0.0:19200->19200/tcp

脚本能跑,功能也正常,但它意味着:这三个端口对所有能访问到这台机器的人都敞开着。为了让它能用,安全组被迫开了三个洞,而洞的源地址是 0.0.0.0/0。

三个具体的风险

第一,反代层的所有能力被跳过。 常见架构是 Nginx → 应用,限流、访问日志、HTTPS、统一的鉴权头都在 Nginx 上。一旦应用端口本身可直连,客户端直接打 19200,等于绕过了整套外围防护。

第二,内网横向移动变得容易。 同网段任何一台机器(包括被入侵的办公电脑、闲置测试机)都能直接连上 REST 网关和 WebSocket,不需要经过任何入口。

第三,安全组规则会失效。 很多内网环境(学校、家庭宽带)出口 IP 是动态的,按源 IP 收紧的规则过几天就不匹配。维护成本一高,最后往往又被改回 0.0.0.0/0。

做法一:把端口收到 loopback

Docker 的 ports 有两种写法,差别很大:

services:
  web:
    ports:
      - "5173:5173"            # 等价于 0.0.0.0:5173 -> 容器 5173
  web-local:
    ports:
      - "127.0.0.1:5173:5173"  # 只监听本机 loopback

后者用 ss -tlnp 看是 127.0.0.1:5173,前者是 *:5173。改完之后,外部(包括同网段)无法直连,只有本机的 Nginx 能访问。

这里有个细节:Docker 通过 iptables 的 DNAT 实现端口映射,绑定 0.0.0.0 时规则对所有网卡生效;指定 127.0.0.1 时只对本机回环生效,不会为外部流量建转发规则。所以从宿主机外部 telnet 内网IP 5173 会直接 connection refused,而不是被防火墙 DROP——排查时这一点很有用。

做法二:Nginx 单入口

server {
    listen 443 ssl http2;
    server_name example.internal;

    location / {
        proxy_pass http://127.0.0.1:5173;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:19200/;
    }

    location /ws {
        proxy_pass http://127.0.0.1:19000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

WebSocket 反代有三项必配,缺一项就出问题:

  1. proxy_http_version 1.1——HTTP/1.0 不支持 Upgrade,漏了会得到 400 或握手失败;
  2. Upgrade 与 Connection 两个头——不带就退化成普通 HTTP 请求;
  3. proxy_read_timeout——默认 60 秒,长连接会被 Nginx 主动断开,表现为“页面放着不动就掉线,刷新又好了”。

代价:前端要改成相对路径

收敛之后最容易踩的坑是前端写死了绝对地址:

const ws = new WebSocket('ws://10.0.0.5:19000/ws');

这样的代码反代之后就废了。正确做法是用相对路径,由浏览器按当前 origin(协议 + 主机 + 端口)拼接:

const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${proto}//${location.host}/ws`);

本地开发时用 Vite 的 server.proxy 把 /ws、/api 转发到后端,线上自然走 Nginx,一份代码两边都能用。

最后的安全组

收敛完成后,安全组只留两条:

  • 22/tcp,源地址填自己的出口 IP,优先密钥登录;
  • 443/tcp,按实际使用者收敛。

规则从此固定,换网络、换教室、换办公室都不用改。把“能用”变成“可维护”,往往就是这一两步的差别。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。