阿里云 ECS 部署 Node 升级服务实战:Ubuntu + systemd + Nginx
摘要
上一篇已经把 APP(QGC/地面站)远程升级服务的核心接口跑起来了,这一篇把它部署到阿里云 ECS 的 Ubuntu 服务器上。
目标是让外网手机可以访问:
text
http://203.0.113.10:8080/fra/check/update
http://203.0.113.10:8080/updates/GCS-Demo-1.03.08.apk
其中 203.0.113.10 是示例 IP,实际发布时换成自己的公网 IP 或域名。
适用场景
- 本地 Node 服务已经调通,需要部署到公网服务器。
- App 端不能再依赖局域网地址。
- 需要 systemd 守护进程,服务器重启后自动恢复。
- 后续可能用 Nginx 把 80 端口转发到 8080。
本文效果
完成后可以做到:
- Ubuntu 服务器上运行 Node 升级服务。
systemctl status guardsky-upgrade显示服务在线。- 公网访问
/health返回{"ok":true}。 - App 可以请求公网升级接口并下载 APK。
- 可选使用 Nginx 让 App 不再带
:8080。
背景
远程升级最容易在部署阶段踩坑:
- 本地
localhost能访问,手机访问不了。 - 云服务器程序启动了,但阿里云安全组没放行端口。
- 服务手动
npm start可以跑,断开 SSH 就停了。 - APK 上传到服务器后,服务端返回的还是局域网地址。
所以正式上线要把几个点串起来:
text
代码上传 -> npm install -> 环境变量 -> systemd -> 安全组 -> 公网验证 -> App 验证
部署流程图
#mermaid-svg-TQEJPqdZUGjrSFoZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TQEJPqdZUGjrSFoZ .error-icon{fill:#552222;}#mermaid-svg-TQEJPqdZUGjrSFoZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TQEJPqdZUGjrSFoZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .marker.cross{stroke:#333333;}#mermaid-svg-TQEJPqdZUGjrSFoZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TQEJPqdZUGjrSFoZ p{margin:0;}#mermaid-svg-TQEJPqdZUGjrSFoZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster-label text{fill:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster-label span{color:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster-label span p{background-color:transparent;}#mermaid-svg-TQEJPqdZUGjrSFoZ .label text,#mermaid-svg-TQEJPqdZUGjrSFoZ span{fill:#333;color:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .node rect,#mermaid-svg-TQEJPqdZUGjrSFoZ .node circle,#mermaid-svg-TQEJPqdZUGjrSFoZ .node ellipse,#mermaid-svg-TQEJPqdZUGjrSFoZ .node polygon,#mermaid-svg-TQEJPqdZUGjrSFoZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .rough-node .label text,#mermaid-svg-TQEJPqdZUGjrSFoZ .node .label text,#mermaid-svg-TQEJPqdZUGjrSFoZ .image-shape .label,#mermaid-svg-TQEJPqdZUGjrSFoZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-TQEJPqdZUGjrSFoZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .rough-node .label,#mermaid-svg-TQEJPqdZUGjrSFoZ .node .label,#mermaid-svg-TQEJPqdZUGjrSFoZ .image-shape .label,#mermaid-svg-TQEJPqdZUGjrSFoZ .icon-shape .label{text-align:center;}#mermaid-svg-TQEJPqdZUGjrSFoZ .node.clickable{cursor:pointer;}#mermaid-svg-TQEJPqdZUGjrSFoZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .arrowheadPath{fill:#333333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TQEJPqdZUGjrSFoZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TQEJPqdZUGjrSFoZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TQEJPqdZUGjrSFoZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster text{fill:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ .cluster span{color:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TQEJPqdZUGjrSFoZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TQEJPqdZUGjrSFoZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-TQEJPqdZUGjrSFoZ .icon-shape,#mermaid-svg-TQEJPqdZUGjrSFoZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TQEJPqdZUGjrSFoZ .icon-shape p,#mermaid-svg-TQEJPqdZUGjrSFoZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TQEJPqdZUGjrSFoZ .icon-shape .label rect,#mermaid-svg-TQEJPqdZUGjrSFoZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TQEJPqdZUGjrSFoZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TQEJPqdZUGjrSFoZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TQEJPqdZUGjrSFoZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地 server 项目
上传到 ECS /opt/gcs-update-server/server
npm install 安装依赖
配置 guardsky-upgrade.env
安装 systemd service
放行 8080/tcp 安全组
curl /health 验证
App 请求 /fra/check/update
下载 /updates/*.apk
如果 Mermaid 在 CSDN 不能直接显示,可以截图后作为流程图插入。
1. 上传项目到服务器
本地目录示例:
text
D:\your_workspace\server
服务器目标目录示例:
text
/opt/gcs-update-server/server
首次部署可以整体上传:
powershell
scp -r D:\your_workspace\server root@203.0.113.10:/opt/gcs-update-server/
如果上传后目录变成了:
text
/opt/gcs-update-server/server/server/server.js
说明多套了一层,需要整理成:
text
/opt/gcs-update-server/server/server.js
/opt/gcs-update-server/server/package.json
/opt/gcs-update-server/server/public/updates/GCS-Demo-1.03.08.apk
后续更新代码时,不建议每次整包覆盖。线上这几个位置通常属于数据文件:
text
/opt/gcs-update-server/server/deploy/guardsky-upgrade.env
/opt/gcs-update-server/server/update-config.json
/opt/gcs-update-server/server/public/updates/
如果只是更新服务代码,优先同步:
powershell
scp D:\your_workspace\server\server.js root@203.0.113.10:/opt/gcs-update-server/server/
scp -r D:\your_workspace\server\public\admin root@203.0.113.10:/opt/gcs-update-server/server/public/
2. 安装依赖
登录服务器:
bash
ssh root@203.0.113.10
进入项目目录:
bash
cd /opt/gcs-update-server/server
npm install
项目依赖很少:
json
{
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cookie-parser": "^1.4.7",
"express": "^4.19.2",
"multer": "^2.1.1"
}
}
Node 建议使用 18 或更高版本。
3. 配置环境变量
项目里准备了一个示例配置:
bash
cd /opt/gcs-update-server/server
cp deploy/guardsky-upgrade.env.example deploy/guardsky-upgrade.env
nano deploy/guardsky-upgrade.env
配置内容:
text
PORT=8080
PUBLIC_BASE_URL=http://203.0.113.10:8080
ADMIN_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD
说明:
PORT:Node 服务监听端口。PUBLIC_BASE_URL:返回给 App 的公网基础地址。ADMIN_PASSWORD:后台登录口令,真实环境必须改强密码,不要提交到 Git。
仓库中的示例文件内容如下:
text
# Copy this file to guardsky-upgrade.env on the cloud server.
# Keep guardsky-upgrade.env out of source control because it contains the admin password.
PORT=8080
PUBLIC_BASE_URL=http://203.0.113.10:8080
ADMIN_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD
4. 安装 systemd 服务
systemd 配置文件:
ini
[Unit]
Description=Guardsky Upgrade Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/gcs-update-server/server
EnvironmentFile=/opt/gcs-update-server/server/deploy/guardsky-upgrade.env
ExecStart=/usr/bin/npm start
Restart=always
RestartSec=3
User=root
[Install]
WantedBy=multi-user.target
安装:
bash
cp /opt/gcs-update-server/server/deploy/guardsky-upgrade.service /etc/systemd/system/guardsky-upgrade.service
systemctl daemon-reload
systemctl enable guardsky-upgrade
systemctl restart guardsky-upgrade
systemctl status guardsky-upgrade
期望看到:
text
Active: active (running)
查看实时日志:
bash
journalctl -u guardsky-upgrade -f
常用命令:
bash
systemctl stop guardsky-upgrade
systemctl start guardsky-upgrade
systemctl restart guardsky-upgrade
systemctl status guardsky-upgrade
5. 放行端口
Ubuntu 系统防火墙如果启用了 ufw:
bash
ufw allow 8080/tcp
ufw status
如果提示 inactive,说明系统防火墙没启用,这一步可以跳过。
但是阿里云 ECS 还必须检查安全组:
text
方向:入方向
协议:TCP
端口:8080
来源:0.0.0.0/0
如果你准备使用 Nginx 的 80 端口,还要放行:
text
TCP 80
6. 服务器本机验证
在 ECS 上执行:
bash
curl http://127.0.0.1:8080/health
期望返回:
json
{"ok":true}
如果本机都不通,先看:
bash
systemctl status guardsky-upgrade
journalctl -u guardsky-upgrade -n 100
常见原因:
deploy/guardsky-upgrade.env不存在。ADMIN_PASSWORD没配置。- Node 没装或
/usr/bin/npm路径不对。 - 8080 端口被别的程序占用。
npm install没执行成功。
7. 公网验证
在本地电脑浏览器打开:
text
http://203.0.113.10:8080/health
期望:
json
{"ok":true}
再验证升级接口:
powershell
Invoke-RestMethod -Method Post `
-Uri http://203.0.113.10:8080/fra/check/update `
-ContentType 'application/x-www-form-urlencoded' `
-Body '&app_ids=com.example.gcs&app_code=10307&app_flavor=release'
如果服务端有更高版本,期望返回:
json
{
"code": 2000,
"msg": "new version available",
"data": {
"versionCode": 10308,
"versionName": "1.03.08",
"fileUrl": "http://203.0.113.10:8080/updates/GCS-Demo-1.03.08.apk"
}
}
APK 下载地址也要能打开:
text
http://203.0.113.10:8080/updates/GCS-Demo-1.03.08.apk
8. 可选:Nginx 80 端口转发
如果不想让 App 地址带 :8080,可以让 Nginx 监听 80,再反向代理到本机 8080。
Nginx 配置:
nginx
server {
listen 80;
server_name 203.0.113.10;
client_max_body_size 1200m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
安装并启用:
bash
apt install -y nginx
cp /opt/gcs-update-server/server/deploy/nginx-ip-http.conf /etc/nginx/sites-available/gcs-upgrade
ln -sf /etc/nginx/sites-available/gcs-upgrade /etc/nginx/sites-enabled/gcs-upgrade
nginx -t
systemctl reload nginx
然后把环境变量改成:
text
PUBLIC_BASE_URL=http://203.0.113.10
App 地址也改成:
cpp
#define STGC_HTTP_ADDRESS "http://203.0.113.10/fra/"
如果后续加域名和 HTTPS,则改为:
cpp
#define STGC_HTTP_ADDRESS "https://upgrade.example.com/fra/"
9. 上线更新注意事项
线上不要随便覆盖这些文件:
text
deploy/guardsky-upgrade.env
update-config.json
public/updates/
原因:
guardsky-upgrade.env里有线上密码和公网地址。update-config.json是后台维护的版本库。public/updates/里是已上传的 APK。
推荐的更新流程:
#mermaid-svg-ErdfTP6PB4sfwHAf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ErdfTP6PB4sfwHAf .error-icon{fill:#552222;}#mermaid-svg-ErdfTP6PB4sfwHAf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ErdfTP6PB4sfwHAf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ErdfTP6PB4sfwHAf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ErdfTP6PB4sfwHAf .marker.cross{stroke:#333333;}#mermaid-svg-ErdfTP6PB4sfwHAf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ErdfTP6PB4sfwHAf p{margin:0;}#mermaid-svg-ErdfTP6PB4sfwHAf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster-label text{fill:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster-label span{color:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster-label span p{background-color:transparent;}#mermaid-svg-ErdfTP6PB4sfwHAf .label text,#mermaid-svg-ErdfTP6PB4sfwHAf span{fill:#333;color:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf .node rect,#mermaid-svg-ErdfTP6PB4sfwHAf .node circle,#mermaid-svg-ErdfTP6PB4sfwHAf .node ellipse,#mermaid-svg-ErdfTP6PB4sfwHAf .node polygon,#mermaid-svg-ErdfTP6PB4sfwHAf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ErdfTP6PB4sfwHAf .rough-node .label text,#mermaid-svg-ErdfTP6PB4sfwHAf .node .label text,#mermaid-svg-ErdfTP6PB4sfwHAf .image-shape .label,#mermaid-svg-ErdfTP6PB4sfwHAf .icon-shape .label{text-anchor:middle;}#mermaid-svg-ErdfTP6PB4sfwHAf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ErdfTP6PB4sfwHAf .rough-node .label,#mermaid-svg-ErdfTP6PB4sfwHAf .node .label,#mermaid-svg-ErdfTP6PB4sfwHAf .image-shape .label,#mermaid-svg-ErdfTP6PB4sfwHAf .icon-shape .label{text-align:center;}#mermaid-svg-ErdfTP6PB4sfwHAf .node.clickable{cursor:pointer;}#mermaid-svg-ErdfTP6PB4sfwHAf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ErdfTP6PB4sfwHAf .arrowheadPath{fill:#333333;}#mermaid-svg-ErdfTP6PB4sfwHAf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ErdfTP6PB4sfwHAf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ErdfTP6PB4sfwHAf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ErdfTP6PB4sfwHAf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ErdfTP6PB4sfwHAf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ErdfTP6PB4sfwHAf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster text{fill:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf .cluster span{color:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ErdfTP6PB4sfwHAf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ErdfTP6PB4sfwHAf rect.text{fill:none;stroke-width:0;}#mermaid-svg-ErdfTP6PB4sfwHAf .icon-shape,#mermaid-svg-ErdfTP6PB4sfwHAf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ErdfTP6PB4sfwHAf .icon-shape p,#mermaid-svg-ErdfTP6PB4sfwHAf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ErdfTP6PB4sfwHAf .icon-shape .label rect,#mermaid-svg-ErdfTP6PB4sfwHAf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ErdfTP6PB4sfwHAf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ErdfTP6PB4sfwHAf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ErdfTP6PB4sfwHAf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地修改 server.js / admin
本地 npm start 验证
scp 指定文件到 ECS
systemctl restart guardsky-upgrade
curl /health
调用 /fra/check/update
App 真机验证
常见问题
1. ECS 上 curl 127.0.0.1 可以,公网不通
优先检查:
- 阿里云安全组是否放行 8080。
- Ubuntu
ufw是否放行。 - 服务是否监听在
0.0.0.0,不要只监听127.0.0.1。
2. App 能检查到更新,但 APK 下载失败
看接口返回的 fileUrl。如果里面是:
text
http://localhost:8080/updates/xxx.apk
那手机一定下载不了。应该配置:
text
PUBLIC_BASE_URL=http://203.0.113.10:8080
3. 后台上传大 APK 失败
如果走 Nginx,需要设置:
nginx
client_max_body_size 1200m;
否则大包可能被 Nginx 拦掉。
小结
这篇把 Node 升级服务从本地搬到了阿里云 ECS。核心不是命令多复杂,而是要把链路验证完整:
text
systemd 在线 -> 本机 health -> 公网 health -> 升级接口 -> APK 下载 -> App 真机
下一篇继续写后台管理页面,看看怎么通过 Web 页面维护多应用版本、上传 APK,并自动选择当前生效版本。