周一早上九点,我把项目在本机跑了一遍,接口通、页面通、数据也通。十点主管说让隔壁组几个同事先在局域网里试用一下,我把地址从localhost改成自己电脑的内网 IP 发过去,对方回了三个字:打不开。这大概是每个做交付的人都会遇到的一幕——项目打包部署、局域网部署、防火墙端口设置这三件事,任何一环没对上,链接就是死的。本机跑通只证明代码没写错,它离“别人能在同一张网里访问”还差着打包产物一致性、监听地址、端口放行、进程托管这好几道关。这篇内容我按自己踩过的顺序拆开讲:怎么打出一个能在别人机器上原样跑起来的包,怎么在局域网里选地址、收敛端口、把进程挂住不挂掉,以及防火墙到底该在哪一层、用什么命令、放行到什么粒度。适合刚接手内网交付的开发,也适合被“我这边能开啊”这句话折磨过的运维。命令和配置都是可以直接抄的,参数为什么这么填我会一并说清楚。
1. 先理清链路:局域网部署到底卡在哪一步
1.1 从“本机能跑”到“别人能开”的四个断点
绝大多数“打不开”都能定位到四个断点之一,而且顺序是固定的:进程根本没起来;进程起来了但只绑在回环地址上;绑对了但主机防火墙把包丢了;包到了但网络本身不通(跨网段、AP 隔离、虚拟机网络模式不对)。这四层的排查成本是递增的,所以永远从最便宜的往下走。
我见过太多人一上来就怀疑防火墙,结果查了半天发现是容器只监听了127.0.0.1。也见过反过来的,服务老老实实绑了0.0.0.0,端口也开着,就是连不上,最后一查是同办公室的无线网络开了客户端隔离。判断顺序错了,时间全浪费在猜上。
下面这张表是我自己贴在便签上的对照表,症状和层级基本一一对应:
| 现象 | 大概率层级 | 第一手验证手段 |
|---|---|---|
| 浏览器提示连接被拒绝 | 进程未启动或端口没监听 | ss -tlnp/netstat -ano |
| 本机能开,内网 IP 打不开 | 监听地址绑错 | 看监听是0.0.0.0还是127.0.0.1 |
| 本机内网 IP 能开,别人打不开 | 主机防火墙拦截 | 临时加规则验证,再收紧 |
| ping 得通但端口连不上 | 防火墙丢包或服务未监听 | telnet/Test-NetConnection探端口 |
| ping 都不通 | 网络层不通 | 查网段、网关、AP 隔离、虚拟网络模式 |
1.2 为什么不建议一上来就关防火墙
刚入行那会儿我图省事,直接在测试机上把防火墙停了,问题确实没了,人也就懒了。代价是后面每次换机器都要重复一遍这个动作,而且交付到对方环境时被安全规范卡住,我得回头重新做一遍最小放行,等于返工两次。
局域网不等于可信网络。办公网里什么设备都有,随手插的路由器、装了一堆来路不明软件的终端、共享打印机,都可能在同一广播域里。数据库端口对全网段敞开,等于把家底摆在走廊上。我的做法是固定三条:
- 先关掉“全关”和“全开”这两个极端,只做按端口、按来源、按协议的最小放行。
- 面向使用者的端口(通常是 80 或某个高位端口)放开到业务网段,管理端口只给自己那台机器的 IP。
- 每次改完规则立刻记录一行:哪天、为谁、开了什么、有效期到什么时候。这条看着像形式主义,实际是三个月后你唯一能还原现场的东西。
另外提一句协议的选择。绝大多数 Web 服务走 TCP,放行时明确写tcp,不要图省事写both,UDP 那条口子开出去没有任何业务收益,只会多一个被扫描的面。
2. 打包:让产物在另一台机器上原样跑起来
2.1 前端打包最容易忽略的三个隐性依赖
前端打完包扔进dist目录,看上去最省心,实际上坑都藏在构建时的环境变量里。第一个坑是接口地址写死。开发时接口走的是本地转发,构建产物里如果还是http://localhost:8080,到了同事的浏览器里就指向他自己的电脑,必然 404。解决办法是在构建配置里用环境变量注入,生产环境指向相对路径/api,由统一入口去转发,这样换机器、换 IP 都不用重新打包。
第二个坑是路由模式。用 history 模式的路由,刷新子页面会 404,因为服务器上并不存在那个路径对应的文件。这个不是打包问题,是服务端要配合把未命中的路径回落到index.html,后面配置里我会写。
第三个坑是静态资源的 base path。项目如果部署在子路径下(比如http://192.168.1.50/app/),构建时的 publicPath 必须跟着改,否则 JS 和 CSS 全是 404,页面白屏但控制台一堆红。
# 典型的前端生产构建 npm ci npm run build -- --mode production # 产物检查:确认没有残留 localhost grep -r "localhost" dist/ | head最后那条grep是我必做的动作,一条命令就能提前发现写死的地址,比等到对方截图过来再查快得多。
2.2 后端打包:不同技术栈的产物形态差异
后端打包的核心目标是产物自包含,也就是目标机器上不需要装一整套构建工具链。Java 系最舒服,mvn package出来一个可执行 jar,只要目标机器有对应大版本的 JRE 就能跑,注意编译时的 JDK 版本不要高于运行时的 JRE 版本,否则启动直接报不支持的类文件版本。我一般会把-DskipTests加上,但只在明确知道测试覆盖了哪些逻辑时用,正式交付前还是跑一遍。
Python 系要谨慎。用 PyInstaller 打成单文件看着很美,实际遇到动态导入、数据文件路径、C 扩展时会连环出问题,而且排查成本极高。我的常规选择是带着源码走,目标机器上建虚拟环境装依赖,把requirements.txt的版本号全部锁死(用pip freeze导出),再用 systemd 挂起来。稳定性比“看起来只有一个文件”重要得多。
Node 系分两种:纯前端静态产物最简单;如果是 Node 后端服务,要么把node_modules一起带上(体积大但省事),要么在目标机器上npm ci --omit=dev重装。前提是目标机器能访问到内部的软件源,如果完全离线,就得提前把依赖包缓存好带过去。
2.3 打包清单:一份能减少八成返工的核对表
我习惯在打包结束时对着清单过一遍,五分钟能省掉后面两小时的扯皮:
| 检查项 | 具体要求 | 常见翻车点 |
|---|---|---|
| 运行环境版本 | 目标机器的 JDK/Python/Node 大版本与构建机一致 | 版本高一点低一点直接起不来 |
| 配置文件外置 | 数据库地址、端口、上传路径抽到配置文件 | 打进包里,换环境要重新构建 |
| 数据库脚本 | 只带结构不带数据,字符集统一 utf8mb4 | 中文乱码、时间差 8 小时 |
| 上传目录 | 独立目录并预留写权限 | 权限不足导致上传静默失败 |
| 日志目录 | 单独目录,配好滚动策略 | 日志写满磁盘把服务拖死 |
| 启动脚本 | 明确工作目录,用绝对路径 | 相对路径在 systemd 下全部失效 |
| 端口约定 | 与后续防火墙放行保持一致 | 改了端口忘了改规则 |
这里面我最想强调“配置文件外置”和“工作目录用绝对路径”这两条。前者决定了你换一次环境要不要重新打包,后者决定了服务在手动启动时正常、挂成系统服务后立刻找不到文件——因为 systemd 的默认工作目录是根目录,所有相对路径都会失效。
3. 部署到局域网机器:地址、端口与进程托管
3.1 内网地址规划:别让 IP 明天就变了
局域网部署第一个要定的是地址稳定性。DHCP 分配的地址有租期,机器重启或者租期到了可能就换一个,你昨天发出去的链接今天就失效。两个做法:一是在目标机器上配静态 IP,二是在路由器上做地址绑定(把网卡 MAC 和固定 IP 关联),后者更省事,也不用改机器配置。地址选在网段的可用范围里,避开网关和已占用地址,比如192.168.1.50这种远离 DHCP 池的位置。
如果内网有可用的域名解析,给这台机器加一条记录会舒服很多,同事记住app.internal比记一串数字容易。没有条件就退一步,在常用几台机器的 hosts 文件里加一行映射,成本极低但体验提升明显。不过 hosts 方案不适合大规模推广,人一多就该考虑正经的内网解析了。
3.2 0.0.0.0 和 127.0.0.1:一个字符之差,半天工时
这是局域网部署里出现频率最高的错误,没有之一。127.0.0.1是回环地址,只有本机能访问;0.0.0.0表示监听本机所有网卡,外部请求才能进来。很多框架的开发模式默认绑回环,是为了安全,但部署时必须显式改掉。
不同技术栈的写法:
# Spring Boot java -jar app.jar --server.address=0.0.0.0 --server.port=8080 # Flask app.run(host="0.0.0.0", port=8080) # Vite 预览 / 开发服务器 npm run preview -- --host 0.0.0.0改完立刻验证监听状态,别靠猜:
# Linux ss -tlnp | grep 8080 # 输出 0.0.0.0:8080 才算对,127.0.0.1:8080 就是还在绑回环 # Windows netstat -ano | findstr :8080还有一种更隐蔽的情况:监听显示的是[::]:8080,也就是只绑了 IPv6。某些环境里 IPv6 栈和 IPv4 栈的行为不一致,导致部分客户端连不上。稳妥做法是让服务同时监听两个协议族,或者在配置里明确写0.0.0.0而不是留空。
端口占用也顺手查一下。Linux 上看ss -tlnp最后一列就是进程名和 PID,直接定位;Windows 上netstat -ano拿到 PID 后用任务管理器或tasklist反查。Windows 还有个特产坑:Hyper-V 之类的虚拟化组件会动态保留一批端口段,服务绑定某个端口时报“权限不足”,其实是撞进了保留区。
# 查看 Windows 的动态保留端口段 netsh int ipv4 show excludedportrange protocol=tcp遇到这种情况,换端口是最省事的,非要保留原端口就得调整动态端口范围并重启,属于下策。
3.3 端口收敛:对外只留一个入口
前后端分离的项目直接暴露两个端口(前端 3000、后端 8080),意味着防火墙要开两个口,跨域要单独处理,证书要管两份。我的习惯是在目标机器上放一个统一入口,对外只开一个端口,前端静态资源和接口转发都由它处理。
server { listen 80; server_name _; root /opt/app/web; index index.html; # 前端 history 路由回落,解决刷新 404 location / { try_files $uri $uri/ /index.html; } # 接口请求转给本机后端进程 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } # 上传大小上限,默认 1M 会直接 413 client_max_body_size 50m; }三个参数值得单独说。try_files那行是前端路由能刷新正常的关键;proxy_pass结尾那个斜杠决定路径是否被截断,带斜杠会把/api/前缀去掉再转发,写错就是接口全线 404;client_max_body_size不设,用户传一张大图就会收到 413,而这个错误在前端往往只显示“上传失败”,排查方向容易跑偏。
收敛成一个端口还有个额外好处:后端进程只需要监听127.0.0.1,压根不对外暴露,攻击面直接小一圈。防火墙那边也就只需要放行一个端口,规则表干净,日后交接也说得清楚。
3.4 进程托管:让服务在关掉终端后继续活着
手动java -jar或者npm start起的进程,终端一关就没了,这是新手最常见的“服务自己挂了”。正经做法是交给系统级的服务管理器。
Linux 上用 systemd,写一个 unit 文件:
# /etc/systemd/system/myapp.service [Unit] Description=myapp backend After=network.target [Service] Type=simple WorkingDirectory=/opt/app ExecStart=/usr/bin/java -jar /opt/app/app.jar Restart=always RestartSec=5 User=appuser Environment=JAVA_OPTS=-Xms256m -Xmx512m StandardOutput=append:/var/log/myapp/out.log StandardError=append:/var/log/myapp/err.log [Install] WantedBy=multi-user.targetsystemctl daemon-reload systemctl enable --now myapp systemctl status myapp journalctl -u myapp -fWorkingDirectory和绝对路径这两点前面提过,是这个文件里最容易错的地方。Restart=always加上RestartSec=5能避免进程崩溃后无限快速重启把 CPU 打满。User=appuser是安全习惯,不要用 root 跑业务进程。
Windows 上官方没有等价物,常用做法是借助第三方工具把可执行程序注册成服务,或者用计划任务设置“开机时运行”。计划任务的坑在于它默认不等待网络就绪,服务启动时数据库还没通,于是起来就挂。稳妥点还是用服务包装工具,配置好工作目录和日志输出再注册。
4. 防火墙端口设置:把门开在该开的位置
4.1 先分清是哪一层在挡
“防火墙”这个词在局域网部署里至少指三个不同的东西,搞混了就会在错误的层反复折腾:
| 层级 | 典型形态 | 排查方式 |
|---|---|---|
| 主机防火墙 | Linux 的 firewalld/ufw/iptables,Windows Defender 防火墙 | 本机上看规则列表 |
| 虚拟化/容器层 | 容器的端口映射、虚拟机的网络模式 | 看映射参数和网卡配置 |
| 网络设备策略 | 交换机的访问控制、无线的客户端隔离 | 换一台机器、换一种接入方式对比 |
判断方法很朴素:如果同一台机器上本机访问内网 IP 通、别的机器不通,问题在主机防火墙;如果容器内能访问、宿主机外部不通,问题在容器映射;如果有线能通、无线不通,去查无线的客户端隔离。分层验证比盲改规则高效得多。
4.2 Linux 主机放行实操
用 firewalld 的发行版(openEuler、CentOS 系常见):
# 查看当前放行的端口和服务 firewall-cmd --list-all # 放行 80 端口,永久生效并立即加载 firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload # 更精细:只允许业务网段访问 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="8080" protocol="tcp" accept' firewall-cmd --reload--permanent和--reload这一对是必须成对使用的,只加不 reload 不生效,只 reload 不加则重启后丢失。这个细节我在早期文档里从没见人强调过,但它造成的“明明加了规则还是连不上”特别多。
用 ufw 的发行版(Ubuntu 系常见):
ufw status verbose ufw allow from 192.168.1.0/24 to any port 80 proto tcp ufw reloadufw 的规则顺序是从上往下匹配,deny写在前面会把后面的allow全盖掉,所以改完一定要ufw status numbered看一眼实际顺序,别只看自己敲了什么。
直接操作 iptables 的场景要更小心,规则不持久化,重启就没了,而且顺序敏感,插错位置可能把自己锁在外面。非必要不用这一层,非用不可时先确认有带外访问手段。
还有两个 Linux 特有的坑。一是 SELinux,它默认只允许 Web 服务绑定 80、443 这类被标记过的端口,你把服务放到 8081 上,防火墙开了也连不上,因为进程压根没绑成功:
# 查看端口是否被允许绑定 semanage port -l | grep http_port_t # 追加自定义端口 semanage port -a -t http_port_t -p tcp 8081二是容器映射。用容器跑服务时,-p 8080:8080会在底层插入自己的转发规则,优先级高于主机防火墙的入站规则,也就是说你以为 ufw 挡住了,其实容器端口是全开的。稳妥做法是把映射绑到回环上,让统一入口去转发:-p 127.0.0.1:8080:8080。这个细节很少有人提,但只要用过容器部署基本都踩过。
4.3 Windows 主机放行实操
图形界面路径是“高级安全 Windows Defender 防火墙 → 入站规则 → 新建规则”,选端口、填 TCP、填具体端口号,一路下一步。界面操作直观,但容易漏掉最后一步的“配置文件”选择——规则只对勾选的网络类型生效。办公网通常被识别为“专用网络”,如果机器被误判成“公用网络”,规则就不匹配,表现就是规则明明在列表里却不起作用。
命令行更快,也方便记录和复现:
# 只允许业务网段访问 8080 New-NetFirewallRule -DisplayName "app-8080" -Direction Inbound ` -Protocol TCP -LocalPort 8080 -Action Allow ` -Profile Private -RemoteAddress 192.168.1.0/24 # 查看规则 Get-NetFirewallRule -DisplayName "app-8080" | Format-List # 删除规则 Remove-NetFirewallRule -DisplayName "app-8080"老一些的环境用 netsh 也能达到同样效果:
netsh advfirewall firewall add rule name="app-8080" dir=in action=allow protocol=TCP localport=8080 profile=private remoteip=192.168.1.0/24 netsh advfirewall firewall show rule name="app-8080"这两个命令我都留了profile=private和remoteip限定。不加的话规则默认对所有配置文件和所有来源生效,等于把端口摊在整个网络上,虽然局域网风险相对可控,但没有理由不收紧。
4.4 端口选择的几条经验
低于 1024 的端口需要特权,非必要不用,出问题时排查会多一层权限因素。常见 Web 端口里,8080、3000、5000、8000 冲突概率都很高,尤其是 5000,在 Windows 上经常被系统组件占着。我的选择习惯是:对外入口用 80(或 8080),后端内部端口挑一个四位数的高位端口,比如 18080 这种,冲突概率低,一眼就能看出是自定义服务。
放行粒度上,能给网段就不要给全网,能给单 IP 就不要给网段。数据库、缓存这类只服务本机进程的端口,压根不需要过防火墙,把它们绑在127.0.0.1上比配规则更彻底——不存在的入口是防不住攻击的,也是最高效的。
5. 逐层排查实录与常见问题速查
5.1 五步定位法:从最便宜的一步开始
遇到“打不开”,我固定走这五步,基本十五分钟内能定位:
第一步,在服务器本机执行curl -I http://127.0.0.1:端口。通了说明进程正常,不通则是进程问题,去看日志,别往下走。
第二步,在服务器本机执行curl -I http://内网IP:端口。这一步通不过,几乎一定是绑定地址的问题,回去查监听状态。
第三步,在同网段的另一台机器上ping 内网IP。ping 不通说明网络层就不通,跟端口没关系,去查网段、网关、无线隔离、虚拟网络模式。
第四步,探端口。Linux 用nc -zv 内网IP 端口,Windows 用Test-NetConnection 内网IP -Port 端口。ping 通但端口不通,就是防火墙或者服务监听的问题,此时回到第一步确认监听,再查规则。
# Windows 探端口 Test-NetConnection 192.168.1.50 -Port 8080# Linux 探端口 nc -zv 192.168.1.50 8080 curl -v telnet://192.168.1.50:8080第五步才是浏览器。浏览器是最不可靠的验证工具,缓存、扩展、内置的拦截策略都会干扰判断,命令行通而浏览器不通,先清缓存或者开无痕窗口试一次。
5.2 常见问题速查表
| 现象 | 可能原因 | 验证方式 | 处理 |
|---|---|---|---|
| 本机通,他人不通,端口探测超时 | 主机防火墙未放行 | 临时停防火墙对比 | 加最小化入站规则 |
| 本机内网 IP 就访问不了 | 服务绑了 127.0.0.1 | ss -tlnp看监听地址 | 改为 0.0.0.0 |
| 端口探测被拒绝(非超时) | 服务未监听该端口 | 查端口占用 | 换端口或启动服务 |
| 加了规则仍不生效 | 未 reload,或配置文件名不匹配 | --list-all/ 看规则配置文件列 | 重新加载,勾对网络类型 |
| 容器外部访问不到 | 映射绑到了 127.0.0.1 | 看-p参数 | 改绑 0.0.0.0 或用统一入口 |
| 服务一会儿就挂 | 手动启动,终端关闭 | 看是否前台运行 | 用服务管理器托管 |
| 页面白屏,控制台 404 | 静态资源路径前缀不对 | 看请求的 JS 路径 | 调整构建 base path |
| 刷新子页面 404 | history 路由无回落 | 直接访问子路径 | 配置try_files |
| 上传大文件失败 | 请求体大小限制 | 看响应码 413 | 调client_max_body_size |
| 中文乱码、时间偏差 | 数据库字符集/时区 | 查建库语句与连接串 | 统一 utf8mb4 与时间 zone |
5.3 几个容易忽略的现场细节
无线客户端隔离。同一个无线网络下的设备互相访问不了,但都能上网,这是接入设备开了客户端隔离。判断方法很简单:换一根网线接同一台交换机再试,通了就是无线的问题。这个坑在没有专门 IT 支持的办公环境里出现频率极高,而它跟防火墙一点关系都没有,查错方向能浪费一整天。
双网卡绑错网卡。服务器插了两根网线、分属两个网段时,服务监听0.0.0.0会在两个网卡上都可达,但同事的机器在另一个网段,中间没有路由就不通。这种情况要么跨网段打通路由,要么明确告知使用哪个地址。别小看这点,我在一个实验室环境里就因为一台机器有无线和有线两个地址,把链接发成了无线网段那个,对方连了一天。
虚拟机的网络模式。用虚拟机做部署机时,NAT 模式下外部机器根本访问不到虚拟机的服务,必须改成桥接,让虚拟机在局域网里拿到一个独立地址。这个改动只需要点两下,但不知道的人会以为是防火墙问题。
浏览器的自动填充与旧缓存。前一次访问留下的重定向或者缓存页面,会让新部署的版本看起来“没生效”。排查时用无痕窗口,能排除掉一大类假故障。
日志位置。部署时如果没约定日志目录,出问题时连看哪儿都不知道。我的硬性要求是日志统一落在/var/log/应用名/下并配好滚动,出问题第一件事就是tail -f,比任何猜测都有用。
6. 一份可以直接照抄的上线自检流程
把上面这些串成一条固定动作,每次部署照做,能省掉大量来回。先确认目标机器的运行环境版本,解包到约定的绝对路径,改好配置里的数据库地址和监听参数。然后手动前台启动一次,确认能起来、日志没有报错、curl 127.0.0.1能返回。接着改成后台托管方式,注册服务、设置开机自启、重启一次验证真的能自启。再确认监听地址是0.0.0.0(如果是统一入口模式,后端绑回环、入口绑全网卡)。然后按最小粒度加防火墙规则,加完必须 reload,再用--list-all或规则列表确认真的生效。最后从另一台机器走一遍完整链路:ping、探端口、命令行请求、浏览器访问,四个都过才算交付完成。
这套流程里我最看重“重启一次验证自启”这一步。很多人配了enable就以为完事,实际上配置文件里有拼写错误时,enable是成功的,重启后服务却没起来,而这个错误只有真重启一次才会暴露。部署当天重启一次,比交付第二天早上被叫起来处理要划算太多。
最后分享一个我用了很久的小技巧:把这次部署的所有关键信息——机器 IP、端口、防火墙规则、服务名、日志路径、配置文件名——写成一页纸放在项目根目录,命名成DEPLOY.md。下次换人接手,或者三个月后自己要改端口,打开这一页就够了,不用再去翻聊天记录猜当时是怎么配的。这页纸的成本是十分钟,收益是后面每一次维护都不需要重新推理一遍现场。
我个人在实际操作中的体会是,局域网部署真正难的不是某条命令,而是把“本机能跑”和“别人能用”之间的每一层都当成独立的问题去看。打包保证产物一致,监听保证入口可达,防火墙保证放行精准,托管保证进程活着,四件事各自成立,整体才成立。任何一环偷懒,最后都会以“打不开”这三个字的形式回来找你。