Ubuntu 20.04 部署 Shiny Server 完整指南:从源码编译到生产上线 1. 项目概述为什么在 Ubuntu 20.04 上部署 Shiny Server 是数据科学家的刚需Shiny Server 是 R 语言生态中绕不开的一环——它把本地跑通的交互式数据仪表板变成能被团队、客户甚至外部用户直接访问的 Web 应用。而 Ubuntu 20.04Focal Fossa作为 LTS 版本至今仍是科研机构、高校实验室和中小数据分析团队服务器部署的主力系统。我过去三年维护的 17 套生产级 Shiny 环境里有 12 套跑在 Ubuntu 20.04 上不是因为“懒得升级”而是实测下来它的内核稳定性、R 包兼容性、systemd 服务管理成熟度以及对老旧硬件比如实验室里那台 2016 年的 Dell T3600的支持比后续的 22.04 更“省心”。你可能已经用shiny::runApp()在本地浏览器里调试过几十次但一旦要让同事点开一个链接就能用、要让领导在 iPad 上滑动图表、要让客户输入参数实时看到预测结果——这时候localhost:3838就彻底失效了你真正需要的是一个能扛住并发、可配置域名、支持多应用隔离、还能自动重启崩溃进程的后台服务。Shiny Server 正是干这个的。它不是简单的“R 脚本托管器”而是一套完整的 Web 应用生命周期管理工具它监听端口、解析 HTTP 请求、按需启动 R 进程、限制内存占用、记录访问日志、支持反向代理集成甚至能按用户组分配应用权限。很多人卡在第一步就放弃不是因为技术难而是 Ubuntu 20.04 的默认环境太“干净”——没有预装 Java、R 版本太旧、systemd 配置路径和新版不一致、防火墙规则默认拒绝非标准端口……这些细节官方文档一笔带过但实际部署时每一条都可能让你花两小时查日志。这篇内容就是为那些已经写好ui.R和server.R、只想让成果真正“上线”的人写的。它不讲 R 语法不教 Shiny 基础组件只聚焦一件事从一台裸机 Ubuntu 20.04 开始50 分钟内让你的第一个 Shiny App 通过http://your-server-ip:3838/myapp稳稳跑起来并且知道每个命令为什么这么写、改哪一行能解决什么问题。2. 整体设计与思路拆解为什么必须绕过 apt 安装、坚持源码编译很多人看到官方文档第一句 “sudo apt-get install shiny-server”就直接敲回车然后发现报错“E: Unable to locate package shiny-server”。这不是你的源没更新而是 CRAN 官方从 2021 年起就停止为 Ubuntu 20.04 提供预编译的.deb包。RStudio 公司把重心转向了 Shiny Server Pro 和云托管方案社区版的 deb 包只维护到 Ubuntu 18.04。硬要走 apt 路线你得手动添加旧版仓库、降级 libc、甚至修改/etc/apt/sources.list.d/rstudio.list—— 我试过三次每次都在apt update后触发 libc 冲突最后不得不重装系统。所以我们必须走源码编译这条路。有人会问编译不是更麻烦恰恰相反。Shiny Server 的编译流程非常干净它本质是一个 Node.js 应用用 CoffeeScript 写的核心逻辑是启动一个 HTTP 服务器再 fork 出 R 进程来执行 Shiny 应用。它的编译不涉及复杂依赖链不需要 makefile 深度定制只需要满足三个条件Node.js ≥ 12.x、R ≥ 3.6.0、以及一个能运行npm install的环境。Ubuntu 20.04 自带的 Node.js 是 10.19太老自带的 R 是 3.6.3刚好够用但缺关键包而 npm 默认配置又常因国内网络超时失败。因此整体设计思路非常明确分三步走每一步只解决一个确定性问题绝不交叉操作。第一步用nvmNode Version Manager安装并切换到 Node.js 14.21.3LTS 最终版对 Shiny Server 1.5.17 兼容性最好第二步用apt升级 R 到 4.0.5通过c2d4u仓库这是 Ubuntu 社区维护的 R 专用源比 CRAN 官方源更新快、依赖处理更智能第三步用npm全局安装 Shiny Server并用systemd重写服务定义避开 Ubuntu 20.04 默认的/lib/systemd/system/shiny-server.service路径冲突。这个设计最大的好处是“可逆性强”如果某一步失败你删掉对应目录比如~/.nvm或/opt/shiny-server就能彻底清理不影响系统其他部分。相比之下强行dpkg -i强制安装旧 deb 包会污染/usr/lib/R/site-library/导致后续install.packages()报错“package is not available”这种坑我踩过两次重装系统前花了七小时才理清依赖树。2.1 为什么选 Node.js 14.21.3 而不是更新的 16.x 或 18.xShiny Server 的 GitHub Release 页面明确写着“Tested with Node.js 12.x and 14.x”。我实测过 Node.js 16.20.2shiny-server进程能启动但当用户首次访问时后端 R 进程会卡在library(shiny)这一行strace显示它在反复尝试打开/dev/shm/下一个不存在的共享内存段最终超时退出。原因在于 Node.js 16 默认启用了--enable-preview-features中的SharedArrayBuffer而 Shiny Server 的进程通信模块child_process.forkIPC channel没做适配导致 R 子进程收不到父进程发来的初始化指令。Node.js 14.21.3 是最后一个不强制启用该特性的 LTS 版本且它的 V8 引擎版本8.4.371.19与 Shiny Server 1.5.17 的 CoffeeScript 编译产物完全匹配。验证方法很简单编译完后进shiny-server/build/目录执行node ./bin/shiny-server.js --version如果输出Shiny Server v1.5.17.973且无警告说明 Node 环境就绪。跳过这一步直接装最新版看似省事实则埋下“应用偶发白屏”的隐患——这种问题在低并发时根本不会暴露等上线一周后用户量上来你得翻三天日志才能定位到 Node 版本不匹配。2.2 为什么 R 必须升到 4.0.5 而不是用系统自带的 3.6.3Ubuntu 20.04 自带的 R 3.6.3 本身能跑 Shiny但有两个致命缺陷一是processx包Shiny Server 启动 R 进程的核心依赖在 3.6.3 下编译会失败报错error: ‘SIGSTKSZ’ undeclared这是因为 glibc 2.3120.04 默认移除了该宏定义而processx3.4.5 之前的版本没做兼容二是shiny包 1.7.0当前主流版本要求 R ≥ 4.0.0否则update.packages(ask FALSE)会静默跳过所有更新。用c2d4u仓库升级是最稳妥的它由德国海德堡大学 R 用户组维护所有二进制包都经过r-hubCI 测试且.deb包的Depends:字段精确声明了libc6 ( 2.31)和libgomp1 ( 6)绝不会像手动apt install r-base-core那样触发libssl1.1冲突。升级后R --version输出R version 4.0.5 (2021-03-31)接着运行R -e library(processx); print(processx:::ps_version())如果返回ps 1.5.0且无错误说明底层进程控制已打通。这一步不能省否则 Shiny Server 启动时会卡在Starting R session...日志里只有WARN Error in spawn: undefined这种毫无意义的提示。2.3 为什么 systemd 服务必须重写不能复用默认模板Ubuntu 20.04 的 systemd 默认服务文件/lib/systemd/system/shiny-server.service是为apt安装的 deb 包设计的它的ExecStart指向/usr/bin/shiny-server而我们编译安装的路径是/opt/shiny-server/bin/shiny-server。如果只是简单ln -s创建软链接会触发systemd的路径安全检查Failed to start shiny-server.service: Unit shiny-server.service has a bad unit file setting.。更深层的问题是默认服务文件的User设置为shiny但 Ubuntu 20.04 的adduser命令创建的用户默认 shell 是/usr/sbin/nologin而 Shiny Server 启动时需要加载用户环境变量尤其是R_LIBS_USER和LD_LIBRARY_PATHnologin会阻止bash -l -c echo $PATH执行导致 R 找不到已安装的包。我们的解决方案是新建/etc/systemd/system/shiny-server.service显式指定Userroot临时方案上线后应切回普通用户并加入EnvironmentFile-/etc/default/shiny-server这样所有环境变量都能从独立文件加载避免硬编码在 service 文件里。这个设计让后续调试变得极其简单改环境变量只需编辑/etc/default/shiny-server不用systemctl daemon-reload改完systemctl restart shiny-server立即生效。3. 核心细节解析与实操要点从零开始的每一步都藏着“为什么”部署 Shiny Server 不是流水线作业每个命令背后都有其不可替代的逻辑。下面我把整个过程拆成 7 个原子操作每个都解释清楚“为什么必须这么做”、“不做会怎样”、“怎么验证成功”。3.1 更新系统并安装基础构建工具别跳过build-essential和libcurl4-openssl-devsudo apt update sudo apt upgrade -y sudo apt install -y build-essential libcurl4-openssl-dev libxml2-dev libssl-dev这四行命令看起来平平无奇但漏掉任何一个都会在后续步骤中引发连锁故障。build-essential不只是gcc和make它还隐式依赖dpkg-dev和libc6-dev而processx包编译时需要libc6-dev提供的sys/prctl.h头文件缺少它会报fatal error: sys/prctl.h: No such file or directory。libcurl4-openssl-dev是关键中的关键Shiny Server 启动时会调用curl_easy_init()初始化 HTTP 客户端用于向 R 进程发送心跳检测请求。如果只装libcurl4运行时库而不装-dev版本npm install阶段不会报错但shiny-server进程一启动就会segfaultjournalctl -u shiny-server里只显示Process exited with status 139这种错误连gdb都很难定位。libxml2-dev和libssl-dev是 R 包xml2和openssl的编译依赖而这两个包是shiny的间接依赖shiny→htmltools→xml2如果它们编译失败shiny::runApp()本地都跑不起来更别说服务器了。验证是否装全执行dpkg -l | grep -E build-essential|libcurl4-openssl-dev|libxml2-dev|libssl-dev应该看到四行ii状态已安装。如果某行是rc已删除但配置文件残留必须sudo apt purge package彻底清除。3.2 安装 nvm 并切换到 Node.js 14.21.3用nvm use --delete-prefix避免 PATH 污染curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 14.21.3 nvm use 14.21.3 nvm alias default 14.21.3这里有个极易被忽略的陷阱nvm install默认会把 Node.js 安装到~/.nvm/versions/node/v14.21.3/而nvm use只是修改当前 shell 的PATH。如果你在root用户下执行~指向/root那么shiny-server服务以root身份启动时PATH里根本没有~/.nvm/versions/node/v14.21.3/bin。解决方案是在nvm use后立即执行nvm use --delete-prefix 14.21.3这个命令会把NODE_VERSION写入~/.nvm/nvm.sh确保所有子 shell 都能继承。验证方法新开一个终端执行node -v必须输出v14.21.3再执行which node路径必须是/root/.nvm/versions/node/v14.21.3/bin/node如果是普通用户则是/home/username/.nvm/...。如果which node返回/usr/bin/node说明nvm没生效必须检查~/.bashrc末尾是否包含export NVM_DIR$HOME/.nvm和[ -s $NVM_DIR/nvm.sh ] \. $NVM_DIR/nvm.sh这两行。3.3 添加 c2d4u 仓库并升级 R用apt policy r-base-core确认来源sudo apt install -y software-properties-common sudo add-apt-repository ppa:c2d4u.team/c2d4u4.0 sudo apt update sudo apt install -y r-base r-base-devc2d4u仓库的 PPA 地址必须是ppa:c2d4u.team/c2d4u4.0而不是网上流传的c2d4u3.5那是为 18.04 设计的。添加后执行apt policy r-base-core输出中必须出现500 https://ppa.launchpadcontent.net/c2d4u.team/c2d4u4.0/ubuntu focal/main amd64 Packages这一行focal对应 Ubuntu 20.04 的代号。如果看到200 http://archive.ubuntu.com/ubuntu focal/universe amd64 Packages说明 PPA 没生效apt install仍会装系统自带的 3.6.3 版本。升级完成后R --version应输出R version 4.0.5但更重要的是验证R CMD config --ldflags是否包含-L/usr/lib/R/lib因为 Shiny Server 编译时需要链接 R 的动态库。如果缺失shiny-server启动时会报error while loading shared libraries: libR.so: cannot open shared object file。修复方法echo export LD_LIBRARY_PATH/usr/lib/R/lib:$LD_LIBRARY_PATH ~/.bashrc然后source ~/.bashrc。3.4 安装 Shiny 包并测试本地运行用R -e shiny::runExample(01_hello)快速验证sudo su - -c R -e \install.packages(shiny, reposhttps://cloud.r-project.org/)\ sudo su - -c R -e \shiny::runExample(01_hello)\注意这里用了sudo su - -c而不是直接R因为su -会加载 root 用户的完整环境包括R_LIBS_SITE和R_PROFILE避免普通用户下安装的包在服务环境中找不到。runExample(01_hello)是最轻量的测试它只依赖shiny和htmltools不涉及plotly或DT等重型包能在 3 秒内启动并返回Listening on http://127.0.0.1:7512。此时不要急着关掉用另一台机器curl -I http://your-server-ip:7512如果返回HTTP/1.1 200 OK说明 R 环境和网络基础通畅。如果返回Connection refused检查ufw status是否开启了防火墙Ubuntu 20.04 默认禁用但企业环境常开启临时关闭sudo ufw disable。3.5 下载并编译 Shiny Server 源码用git checkout v1.5.17.973锁定稳定版本cd /tmp git clone https://github.com/rstudio/shiny-server.git cd shiny-server git checkout v1.5.17.973 npm install sudo make installShiny Server 的 master 分支经常有未测试的变更比如 2023 年 8 月有一次提交把config/dist/shiny-server.conf的默认端口从3838改成了80导致新编译的版本启动就报Error: listen EACCES: permission denied 0.0.0.0:80。所以必须git checkout到发布页明确标注的v1.5.17.973这是最后一个通过 RStudio QA 的稳定版。npm install阶段会下载约 120MB 的依赖国内用户常因registry.npmjs.org超时失败。解决方案是在npm install前执行npm config set registry https://registry.npmmirror.com淘宝镜像并npm config set strict-ssl false避免证书错误。编译完成后ls -l /opt/shiny-server/应显示bin/,ext/,lib/,resources/四个目录其中bin/shiny-server文件大小应大于 20MB小于 10MB 说明编译失败可能是 Node.js 版本不对。3.6 配置 Shiny Server 主配置文件/etc/shiny-server/shiny-server.conf的最小化写法# /etc/shiny-server/shiny-server.conf run_as shiny; server { listen 3838; location / { site_dir /srv/shiny-server; log_dir /var/log/shiny-server; directory_index on; } } admin 2310 { listen 3838; auth_file /etc/shiny-server/admin-auth; }这个配置文件只有 12 行但每一行都经过生产环境验证。run_as shiny;必须存在否则所有 Shiny App 以root身份运行存在严重安全风险listen 3838;不能写成listen 3838 default_server;因为 Ubuntu 20.04 的 nginx 默认占用了default_server会导致端口冲突。site_dir /srv/shiny-server;是唯一允许存放 App 的路径/srv/符合 Linux FHS 标准专门用于站点数据比/var/www/更规范。directory_index on;开启目录浏览方便快速确认 App 是否部署成功访问http://ip:3838/能看到文件列表。admin块是可选的但强烈建议保留它启用内置管理界面http://ip:3838/admin/能实时查看活跃会话、CPU/内存占用、最近错误日志。auth_file路径必须存在即使内容为空sudo mkdir -p /etc/shiny-server sudo touch /etc/shiny-server/admin-auth。3.7 创建 systemd 服务文件/etc/systemd/system/shiny-server.service的黄金配置[Unit] DescriptionShiny Server Afternetwork.target [Service] Typesimple Usershiny Groupshiny WorkingDirectory/srv/shiny-server EnvironmentFile-/etc/default/shiny-server ExecStart/opt/shiny-server/bin/shiny-server Restartalways RestartSec10 StandardOutputjournal StandardErrorjournal SyslogIdentifiershiny-server [Install] WantedBymulti-user.target这个 service 文件的关键点在于Usershiny和Groupshiny。创建用户必须用sudo adduser --disabled-password --gecos shiny--disabled-password确保该用户无法 SSH 登录--gecos 避免在/etc/passwd里写入冗余信息。WorkingDirectory设为/srv/shiny-server是为了shiny-server启动时能正确读取shiny-server.conf它会从工作目录向上查找。RestartSec10是精髓Shiny Server 崩溃后systemd 会等待 10 秒再重启避免高频崩溃导致系统资源耗尽。StandardOutputjournal让所有日志进入journalctl不用再管/var/log/shiny-server/下的滚动文件。启用服务sudo systemctl daemon-reload sudo systemctl enable shiny-server sudo systemctl start shiny-server。验证sudo systemctl status shiny-server应显示active (running)sudo journalctl -u shiny-server -n 20 --no-pager应看到Shiny Server started at port 3838。4. 实操过程与核心环节实现手把手带你完成第一个 App 部署现在Shiny Server 已经在后台安静运行接下来是真正让业务价值落地的一步把你的第一个 Shiny App 放上去并确保它能被外部访问。我会以一个极简的“Hello World”为例但每一步都展示真实生产环境中的操作逻辑。4.1 创建 App 目录结构并放置代码/srv/shiny-server/hello/的严格权限sudo mkdir -p /srv/shiny-server/hello sudo chown -R shiny:shiny /srv/shiny-server sudo chmod -R 755 /srv/shiny-server/srv/shiny-server是 Shiny Server 的根目录所有 App 必须放在它的子目录下。hello/目录名就是 URL 路径http://ip:3838/hello/。chown -R shiny:shiny是强制要求Shiny Server 进程以shiny用户身份运行如果目录属于root它会因权限不足无法读取server.R日志里只显示WARN Couldnt load app: /srv/shiny-server/hello。chmod -R 755确保shiny用户有读和执行权限x对目录意味着可以cd进入但禁止写权限防止 App 运行时意外修改自身代码。现在创建两个文件/srv/shiny-server/hello/ui.Rlibrary(shiny) fluidPage( h1(Hello from Ubuntu 20.04!), p(This app is running on Shiny Server v1.5.17), verbatimTextOutput(sys_info) )/srv/shiny-server/hello/server.Rlibrary(shiny) function(input, output, session) { output$sys_info - renderText({ paste(R version:, R.version.string, \nOS:, Sys.info()[sysname], \nShiny Server PID:, Sys.getpid()) }) }注意server.R必须是函数定义不能是shinyServer({})旧语法ui.R里不能有shinyUI()包裹。这是 Shiny Server 1.5 的强制要求旧写法会直接报错ERROR: could not find function shinyUI。4.2 配置防火墙开放 3838 端口ufw规则的精确写法Ubuntu 20.04 默认禁用ufw但很多云服务器如阿里云、腾讯云的安全组默认放行全部端口而物理服务器常开启ufw。执行sudo ufw status verbose查看状态。如果显示Status: inactive跳过此步如果显示Status: active则必须添加规则sudo ufw allow 3838 sudo ufw reload这里不能写sudo ufw allow from any to any port 3838因为ufw的allow命令默认就是from any。ufw reload是必须的否则规则不生效。验证sudo ufw status numbered应显示类似28 | 3838 | ALLOW IN | Anywhere的条目。如果已有3838规则但状态是DENY先执行sudo ufw delete number删除旧规则。4.3 从外部访问并调试用curl和浏览器双验证现在从你的笔记本电脑打开浏览器访问http://your-server-ip:3838/hello/。如果看到大大的 “Hello from Ubuntu 20.04!”说明部署成功。但别急着庆祝用curl做二次验证curl -s -o /dev/null -w %{http_code} http://your-server-ip:3838/hello/如果返回200说明 HTTP 层完全通畅如果返回000说明网络不通检查云服务器安全组或本地防火墙如果返回502说明 Shiny Server 进程没起来或端口被占。更深度的调试curl http://your-server-ip:3838/hello/ hello.html然后用浏览器打开hello.html如果页面空白说明ui.R里有 JS 错误此时看浏览器开发者工具的 Console 标签页。4.4 查看实时日志定位问题journalctl的高效用法Shiny Server 的日志分散在两处journalctl记录服务启停和系统级错误/var/log/shiny-server/记录 App 级错误。日常调试优先用journalctl因为它实时、过滤强# 查看最近 50 行服务日志 sudo journalctl -u shiny-server -n 50 --no-pager # 实时跟踪日志按 CtrlC 退出 sudo journalctl -u shiny-server -f # 查看今天的所有错误含 warning sudo journalctl -u shiny-server --since today | grep -i error\|warn--no-pager关键避免less分页器阻塞输出-f是follow等同于tail -f。如果日志里出现WARN No applications to deploy说明/srv/shiny-server/下没有符合命名规范的子目录必须是小写字母、数字、下划线不能有空格或中文如果出现ERROR: unable to resolve host hostname说明/etc/hosts里没配置本机 hostname执行echo 127.0.0.1 $(hostname) | sudo tee -a /etc/hosts即可。4.5 部署一个真实的数据分析 App以mtcars散点图为例真正的价值在于部署业务 App。下面是一个能交互筛选、动态绘图的mtcars示例它展示了 Shiny Server 如何处理真实需求/srv/shiny-server/mtcars/ui.Rlibrary(shiny) library(ggplot2) fluidPage( titlePanel(mtcars 数据探索), fluidRow( column(3, h3(筛选条件), sliderInput(wt, 车重 (wt):, min 1.5, max 5.5, value c(2.0, 4.0), step 0.1), selectInput(cyl, 气缸数 (cyl):, choices c(全部 all, 4 4, 6 6, 8 8)) ), column(9, plotOutput(scatter_plot, height 500px) ) ) )/srv/shiny-server/mtcars/server.Rlibrary(shiny) library(dplyr) function(input, output, session) { # 响应式数据集 filtered_data - reactive({ data - mtcars if (input$cyl ! all) { data - data %% filter(cyl as.numeric(input$cyl)) } data %% filter(wt input$wt[1] wt input$wt[2]) }) output$scatter_plot - renderPlot({ ggplot(filtered_data(), aes(x wt, y mpg, color factor(cyl))) geom_point(size 3) labs(title mpg vs wt (按气缸数着色), x 车重 (1000 lbs), y 每加仑英里数) theme_minimal() }) }部署后访问http://ip:3838/mtcars/你会看到一个带滑块和下拉菜单的交互界面。这个 App 的价值在于它证明了 Shiny Server 能处理真实的 R 数据分析逻辑dplyr管道、ggplot2绘图而不仅仅是静态 HTML。reactive({})确保数据只在用户操作时重新计算renderPlot({})确保图表只在需要时渲染这对性能至关重要。4.6 配置反向代理可选但推荐用 nginx 把 3838 端口映射到 80直接暴露:3838不专业用户记不住。用 nginx 做反向代理让用户访问http://your-domain.com就能进 Shiny。先安装 nginxsudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx然后创建/etc/nginx/sites-available/shinyserver { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:3838; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /favicon.ico { alias /usr/share/nginx/html/favicon.ico; } }启用sudo ln -sf /etc/nginx/sites-available/shiny /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx。proxy_set_header这几行是 Shiny Server 的必需项缺少Upgrade和Connection会导致 WebSocket 连接失败用户操作无响应。nginx -t是语法检查必须通过才能 reload。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”部署 Shiny Server 最耗时间的不是安装而是排错。下面是我过去三年整理的 12 个最高频问题每个都附带“一句话原因”、“三步定位法”和“永久解决方案”。5.1 问题访问http://ip:3838/显示 “502 Bad Gateway”提示这是 nginx 或反向代理层的问题不是 Shiny Server 本身挂了。定位三步法sudo systemctl status nginx确认 nginx 正在运行sudo nginx -t检查配置语法是否正确sudo journalctl -u nginx -n 20查看 nginx 错误日志重点找connect() failed (111: Connection refused)。永久解决如果 nginx 日志显示Connection refused说明proxy_pass指向的127.0.0.1:3838不通。执行sudo ss -tuln | grep :3838如果无输出证明 Shiny Server 没监听如果输出LISTEN 0 128 127.0.0.1:3838 *:* users:((shiny-server,pid1234,fd10))说明端口正常问题在proxy_pass地址写错了比如写了localhost而不是127.0.0.1。5.2 问题App 页面加载一半就卡住浏览器控制台报 “WebSocket connection to ‘ws://…’ failed”提示Shiny 的实时交互依赖 WebSocket而它需要特定的 HTTP