通过CentOS 7虚拟机的Docker服务部署Node-RED及气象数据显示界面搭建完整教程

一、教程概述

本教程适配CentOS 7虚拟机,全程通过Docker服务部署Node-RED工具,解决CentOS 7原生Node.js版本过低、不兼容新版插件的问题。部署完成后,依托Node-RED Dashboard 2.0插件,搭建包含温湿度、光照、风速、风向的城市气象数据可视化界面,包含完整的环境部署、插件安装、节点配置、代码调试步骤,所有操作通俗易懂,适配新手实操。

核心优势:通过Docker容器化部署,实现配置持久化、串口设备兼容、界面可视化,同时规避旧版插件兼容、权限不足、数据丢失等常见问题。

二、前期准备说明

  1. 环境基础:CentOS 7虚拟机已开启网络,可正常访问外网,已安装Docker服务(若未安装需先部署Docker)。

  2. 核心适配:CentOS 7自带Node.js版本过低,无法直接运行Dashboard 2.0插件,因此全程采用Docker镜像部署Node-RED,无需升级系统和本地Node.js版本。

  3. 工具镜像:使用 渡渡鸟镜像同步站提供的Node-RED镜像,下载稳定、不易卡顿。

三、Docker部署Node-RED完整步骤

步骤1:拉取Node-RED官方镜像

镜像采用国内华为云镜像源,下载速度更快、稳定性更高,避免官方外网镜像下载超时问题。

执行拉取命令:

|---------------------------------------------------------------------------------------------------|
| Plain Text docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

通俗解释:该命令的作用是从国内镜像服务器下载最新稳定版的Node-RED程序镜像,相当于提前下载好安装包,为后续创建运行容器做准备。

常见问题解决:若下载过程卡住、进度停滞,是网络波动或服务器限速导致,并非命令错误。直接终止下载后重新执行命令即可,Docker会自动接续未下载完成的部分,不会重复下载完整镜像。

下载完成后,执行以下命令验证镜像是否保存成功:

|------------------------------------------|
| Plain Text docker images | grep nodered |

若输出nodered相关镜像信息,代表下载成功。

步骤2:创建持久化数据目录(避坑关键)

默认Docker容器删除后,所有配置、插件、程序数据都会丢失。提前创建宿主机专属目录,实现数据持久化,同时解决CentOS 7 SELinux权限报错、settings.js加载失败问题。

  1. 创建数据存储文件夹:

|---------------------------------------------|
| Plain Text mkdir -p /root/nodered_data/data |

  1. 开启文件夹全量读写权限,避免权限不足:

|-------------------------------------------------|
| Plain Text chmod -R 777 /root/nodered_data/data |

  1. 永久关闭SELinux安全模块(CentOS 7专属适配):

|-------------------------------------------------------------------------------|
| Plain Text sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config |

通俗解释:以上操作相当于给Node-RED创建一个"专属存储文件夹",后续所有插件、界面配置、程序代码都保存在这里,就算删除、重建容器,数据也不会丢失;关闭SELinux是为了避免系统安全策略拦截容器读写权限。

步骤3:启动Node-RED基础容器

执行创建并启动容器命令,整合端口映射、数据挂载、用户权限配置:

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text docker run -d \ --name node-red \ -p 1880:1880 \ -v /root/nodered_data/data:/data \ --user=root \ swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

参数通俗解释

  • -d:让容器在后台静默运行,不占用终端窗口;
  • --name node-red:给容器命名为node-red,方便后续操作;
  • -p 1880:1880:端口映射,将虚拟机1880端口和容器1880端口绑定,后续通过虚拟机IP访问页面;
  • -v:挂载数据目录,实现配置持久化;
  • --user=root:以最高权限运行容器,避免插件安装、文件读写报错。

启动后验证容器状态,执行:

|----------------------|
| Plain Text docker ps |

若STATUS显示Up,代表容器运行正常,默认访问地址:http://192.168.227.112:1880

步骤4:安装核心插件(Dashboard 2.0+串口插件)

本项目使用Dashboard 2.0制作可视化界面,需单独安装插件,同时安装串口插件适配硬件传感器数据传输,插件会安装在持久化目录,无需重复安装。

  1. 安装Dashboard 2.0可视化插件:

|---------------------------------------------------------------------------------------------|
| Plain Text docker exec -it node-red npm install --prefix /data @flowfuse/node-red-dashboard |

  1. 安装串口通信插件(对接温湿度、光照等传感器):

|-----------------------------------------------------------------------------------------|
| Plain Text docker exec -it node-red npm install --prefix /data node-red-node-serialport |

通俗解释:docker exec命令是直接对正在运行的容器执行操作,无需进入容器内部,一键安装所需插件,操作更便捷。

步骤5:重启容器使插件生效

插件安装完成后必须重启容器,否则无法加载使用:

|------------------------------------|
| Plain Text docker restart node-red |

重启后打开浏览器访问 http://192.168.227.112:1880,若左侧节点栏出现dashboard、serial分类,代表插件安装成功。

步骤6(可选):挂载USB串口硬件设备

若需要连接真实传感器硬件(非模拟数据),需配置串口映射,让容器识别虚拟机的USB串口设备:

  1. 验证虚拟机识别串口设备:

|----------------------------|
| Plain Text ls /dev/ttyUSB0 |

有输出内容则代表硬件识别正常。

  1. 删除旧容器,重建带串口映射的容器:

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text docker rm -f node-red docker run -d \ --name node-red \ -p 1880:1880 \ -v /root/nodered_data/data:/data:Z \ --device=/dev/ttyUSB0:/dev/ttyUSB0:rwm \ --user=root \ swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nodered/node-red:latest |

  1. 验证容器识别串口:

|-----------------------------------------------------|
| Plain Text docker exec -it node-red ls /dev/ttyUSB0 |

输出对应设备地址即挂载成功。

四、Node-RED气象数据界面搭建步骤

步骤1:核心节点选择

本次搭建城市气象数据监测界面,适配四类传感器:温湿度传感器、光照传感器、风速传感器、风向传感器。

选用核心节点:serial in(串口数据接收)、function(数据处理)、template(页面样式/数据展示)、gauge(仪表盘展示)、chart(折线图数据曲线),所有可视化节点均使用Dashboard 2.0自带节点(新旧版dashboard不兼容,仅保留2.0版本)。

步骤2:Dashboard 2.0基础配置

  1. 打开Node-RED界面,左侧菜单找到【Dashboard2.0】,点击【+ UI】创建新UI项目(自定义名称,如my_Dashboard);

  2. 在新建UI下创建两个分组:数据显示 (展示各类传感器数值)、系统状态(展示系统时间、页面状态);

  3. 后续所有展示节点,统一对应绑定两个分组,方便页面分类展示。

步骤3:各节点详细配置

1. serial in****串口节点配置

核心参数:波特率设置为115200,串口选择/dev/ttyUSB0。

通俗说明:该节点是接收硬件传感器传输数据的入口,若找不到该串口,代表虚拟机未连接硬件设备,可直接使用代码模拟数据,不影响界面搭建。

2. function****数据处理节点配置

  1. 基础设置:节点输出数量改为6路,对应温度、湿度、光照、风速、风向、时间6类数据;

  2. 代码配置:粘贴文末完整function代码,核心支持双模式切换

  • 模拟模式(USE_SIMULATION = true):无需硬件,自动生成动态波动的气象数据,适合调试界面;
  • 真实模式(USE_SIMULATION = false):接收串口硬件传感器的真实数据;

关键注意:function节点输出数据必须为纯数字格式,否则仪表盘、折线图无法正常显示数据。

3. template****界面模板节点配置(三大用途)

该节点是页面美化、数据展示的核心,无需复杂操作,直接粘贴对应代码即可:

  • 数据展示模板:绑定【数据显示】分组,粘贴温度、湿度、光照、风速、风向展示代码,自定义数值颜色、字体、布局,直观展示实时数据;
  • 时间采集模板:绑定【系统状态】分组,粘贴时钟代码,生成动态走动的模拟时钟和日期文字;
  • 全局样式模板:类型选择CSS(all page),无需绑定分组,粘贴全局配色代码,将页面改为科技风蓝黑渐变背景,统一界面色调;

简易避坑:template节点下游无其他节点时,无需勾选"Pass through messages from input",不影响页面显示效果。

4. gauge****仪表盘节点配置

  1. 样式选择:half Gauge(半圆仪表盘)、style选择needle(指针式);
  1. 参数设置:自定义尺寸、数值最大值/最小值、物理单位,设置颜色分区区分正常/异常数据;
  1. 分组绑定:归属【数据显示】分组,用于可视化展示风速等数据。

5. chart****折线图节点配置

  1. 基础样式:图表类型选择line折线图,曲线模式选择smooth平滑曲线;
  1. 功能开启:勾选Show Legend显示图例,自定义X/Y轴标签、数值范围;
  1. 美化设置:自定义网格、曲线、文字颜色,自动适配数据格式,用于展示温湿度、光照数据的动态变化趋势。

五、完整节点代码(直接复制使用)

1. Function****节点完整代码

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text // ============================================== // 模式切换开关(只需要改这一个变量) // ============================================== const USE_SIMULATION = true; // true=使用模拟数据 false=使用真实串口数据 // ============================================== // 全局数据存储 // ============================================== if (typeof context.global.sensorData === 'undefined') { context.global.sensorData = { Temperature: 0, Humidity: 0, Illuminance: 0, Wind_speed: 0, Wind_direction: 0 }; } // 初始化缓存和限流标记 if (typeof context.buffer === 'undefined') { context.buffer = ""; context.currentSensor = null; context.lastSendTime = 0; } // ============================================== // 新增:模拟数据生成模块(真实感动态数据) // ============================================== if (USE_SIMULATION) { // 强制限流:每3秒更新一次(和真实硬件保持一致) const now = Date.now(); if (now - context.lastSendTime < 3000) { return null; } context.lastSendTime = now; // 生成有自然波动的真实感数据 const baseTemp = 25; // 基础温度 const baseHumidity = 60; // 基础湿度 const baseLight = 5000; // 基础光照 const baseWindSpeed = 3; // 基础风速 // 添加随机波动(±20%) const temperature = Math.round((baseTemp + (Math.random() - 0.5) * 10) * 10) / 10; const humidity = Math.round(baseHumidity + (Math.random() - 0.5) * 20); const illuminance = Math.round(baseLight + (Math.random() - 0.5) * 4000); const windSpeed = Math.round((baseWindSpeed + (Math.random() - 0.5) * 4) * 10) / 10; const windDirection = Math.round(Math.random() * 360); // 风向转换(和原有代码完全一致) let windDirText = "北风"; if (windDirection >= 337.5 || windDirection < 22.5) windDirText = "北风"; else if (windDirection >= 22.5 && windDirection < 67.5) windDirText = "东北风"; else if (windDirection >= 67.5 && windDirection < 112.5) windDirText = "东风"; else if (windDirection >= 112.5 && windDirection < 157.5) windDirText = "东南风"; else if (windDirection >= 157.5 && windDirection < 202.5) windDirText = "南风"; else if (windDirection >= 202.5 && windDirection < 247.5) windDirText = "西南风"; else if (windDirection >= 247.5 && windDirection < 292.5) windDirText = "西风"; else if (windDirection >= 292.5 && windDirection < 337.5) windDirText = "西北风"; // 输出格式和原有代码100%一致,不需要修改任何连线 return // 1. 温度数据 { payload: temperature }, // 2. 湿度数据(文本+图表) { payload: humidity, topic: "湿度", timestamp: Date.now() }, // 3. 光照强度数据(文本+图表) { payload: illuminance, topic: "光照强度", timestamp: Date.now() }, // 4. 风速数据(文本+仪表盘) { payload: windSpeed }, // 5. 风向数据(文字) { payload: windDirText }, // 6. 时间数据(保留空输出,兼容原有连线) { payload: "" } ; } // ============================================== // 原有串口数据解析代码(完全保留,当USE_SIMULATION=false时自动生效) // ============================================== if (msg.payload) { // 过滤所有乱码字符 let cleanData = msg.payload.replace(/\^0-9a-zA-Z{}":,.\\-/g, ''); context.buffer += cleanData; // 循环处理所有数据片段 while (true) { // 1. 检测传感器名称 const sensorMatch = context.buffer.match(/"(Temperature|Humidity|Illuminance|Wind_speed|Wind_direction)":{/); if (sensorMatch) { context.currentSensor = sensorMatch1; context.buffer = context.buffer.substring(sensorMatch.index + sensorMatch0.length); continue; } // 2. 检测数值并更新 const valueMatch = context.buffer.match(/"value":(0-9.+)/); if (valueMatch && context.currentSensor) { const value = parseFloat(valueMatch1); context.global.sensorDatacontext.currentSensor = value; context.buffer = context.buffer.substring(valueMatch.index + valueMatch0.length); // 强制限流:每3秒只发送一次最新数据 const now = Date.now(); if (now - context.lastSendTime < 3000) { return null; } context.lastSendTime = now; // 提取所有数据 let temperature = Number(context.global.sensorData.Temperature); let humidity = Number(context.global.sensorData.Humidity); let illuminance = Number(context.global.sensorData.Illuminance); let windSpeed = Number(context.global.sensorData.Wind_speed); let windDirection = Number(context.global.sensorData.Wind_direction); // 风向映射 let windDirText = "北风"; if (windDirection >= 337.5 || windDirection < 22.5) windDirText = "北风"; else if (windDirection >= 22.5 && windDirection < 67.5) windDirText = "东北风"; else if (windDirection >= 67.5 && windDirection < 112.5) windDirText = "东风"; else if (windDirection >= 112.5 && windDirection < 157.5) windDirText = "东南风"; else if (windDirection >= 157.5 && windDirection < 202.5) windDirText = "南风"; else if (windDirection >= 202.5 && windDirection < 247.5) windDirText = "西南风"; else if (windDirection >= 247.5 && windDirection < 292.5) windDirText = "西风"; else if (windDirection >= 292.5 && windDirection < 337.5) windDirText = "西北风"; // Dashboard 2.0 专用输出格式 return // 1. 温度 { payload: temperature }, // 2. 湿度(文本+图表) { payload: humidity, topic: "湿度", timestamp: Date.now() }, // 3. 光照(文本+图表) { payload: illuminance, topic: "光照强度", timestamp: Date.now() }, // 4. 风速(文本+仪表盘) { payload: windSpeed }, // 5. 风向 { payload: windDirText }, // 6. 时间 { payload: "" } ; } // 3. 移除无用的闭合符和逗号 if (context.buffer.startsWith('},') || context.buffer.startsWith('}')) { context.buffer = context.buffer.substring(2); context.currentSensor = null; continue; } break; } // 防止缓存溢出 if (context.buffer.length > 1024) { context.buffer = ""; context.currentSensor = null; } return null; } return null; |

2. Template****节点完整代码

(1)数据显示分组代码(温度/湿度/光照/风速/风向)

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text // 温度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">温度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;">{{msg.payload}} °C</div> </div> // 湿度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">湿度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} %RH</div> </div> // 光照强度展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">光照强度</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} lux</div> </div> // 风速展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">风速</div> <div style="font-size:18px; font-weight:500; color:#165DFF;" translate="no">{{msg.payload}} m/s</div> </div> // 风向展示 <div style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:6px; text-align:center;"> <div style="font-size:19px; color:#e8ff38; margin-bottom:4px;">风向</div> <div style="font-size:18px; font-weight:500; color:#165DFF;">{{msg.payload}}</div> </div> |

(2)系统时间采集展示代码

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text <template> <div class="clock-container"> <!-- 左侧模拟时钟 --> <div class="clock"> <div class="clock-face"> <div class="hand hour-hand" :style="{ transform: `rotate({hourDegrees}deg)\` }"\>\ \

{secondDegrees}deg)\` }"\>\ \
\ \ \ \
相关推荐
万叶学编程1 小时前
服务器网站运维指令全解| Linux 实操完整教程
linux·运维·服务器
风曦Kisaki2 小时前
# Kubernetes(K8s)笔记Day13 :工作负载资源概述与 Job (CronJob)控制器
linux·笔记·云原生·容器·kubernetes
vortex52 小时前
ss 命令指南:网络排查神器
linux·运维·网络·终端
Shadow(⊙o⊙)2 小时前
Linux网络部分——基于UDP的四大接口,实例应用,常考汇总,完整服务端、客户端设计流程+代码
linux·网络·udp
深念Y2 小时前
老Flyme 官方 root 开启 ADB TCP 方案
linux·数据库·网络协议·tcp/ip·adb·智能手机·emmc
小此方3 小时前
Re:Linux系统篇(五十二)线程篇 · 五:直击 Linux 内核底层:TCB 与 LWP 一体两面全解析(附自定义线程封装)
linux·运维·驱动开发
Patrick在香港11 小时前
Python Docker镜像从1.2GB到89MB:多阶段构建的完整优化实录
java·python·docker·信息可视化·容器·数据分析·ai编程
ltl11 小时前
校验和与数据完整性:CRC32C、xxHash 与端到端校验
linux
我是小灰灰吖11 小时前
Ubuntu 22.04 设置 CPU 性能模式:提升系统响应速度
linux·ubuntu