城市环境与天气大屏 — 部署运维文档
更新: 7/3/2026字数: 0 字 时长: 0 分钟
City Weather Dashboard 版本 v1.0 | 2026-07-03 运行环境:阿里云 ECS | 部署方式:Docker + Nginx + GitHub Actions
一、项目背景
1.1 项目简介
城市环境与天气大屏(City Weather Dashboard)是一个面向单城市场景的天气与环境可视化大屏项目。项目采用前后端分离架构,以和风天气(QWeather)为主数据源,整合实时天气、空气质量、灾害预警、24 小时趋势、7 天预报、生活指数及周/月统计分析,旨在提供一个"一眼可读"的城市天气状态总览。
项目除主页面大屏外,还包含城市选择抽屉与统计详情页。首次进入页面通过浏览器定位自动解析当前城市,失败时回退默认城市(上海)。所有和风天气请求由后端转发聚合,前端不直连第三方。
1.2 技术栈
| 层级 | 技术选型 |
|---|---|
| 前端框架 | React 18 + Vite + TypeScript |
| 图表 / 样式 | ECharts + SCSS |
| 状态管理 / 路由 | Zustand + React Router |
| 后端 | Node.js + Express + TypeScript |
| 数据库 | SQLite (better-sqlite3, WAL 模式) |
| 数据源 | 和风天气 (QWeather API) — 免费订阅 |
| 部署 | Docker Compose + 宿主 Nginx + GitHub Actions CI/CD |
1.3 项目信息
| 条目 | 内容 |
|---|---|
| 源码仓库 | https://github.com/gouxinjie/weather-dashboard |
| 线上访问 | http://weather.gouxinjie.com |
| ECS 部署路径 | /var/www/weather |
| 容器数 | 2(weather-frontend + weather-backend) |
二、部署架构
2.1 网络拓扑
整体采用「宿主 Nginx 反向代理 + Docker Compose 双容器」方式部署。ECS 80 端口由宿主 Nginx 统一监听,按 server_name 分发到不同业务。weather 前端容器映射宿主机 3200 端口,后端容器仅暴露内部 3201 端口。
请求流向(分层视图):
┌─────────────────────────────────────────────────────────────────────┐
│ 外网用户 │
│ http://weather.gouxinjie.com │
└───────────────────────────┬─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 阿里云 ECS 宿主机 │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 宿主 Nginx (:80) — 统一入口 │ │
│ │ │ │
│ │ server_name _ ──────────────► /var/www/blog (默认站点) │ │
│ │ server_name weather.gouxinjie.com │ │
│ │ │ │ │
│ │ └── proxy_pass ─────────► http://127.0.0.1:3200 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Docker Compose ── 网络: weather-net (bridge) │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ weather-frontend │ │ weather-backend │ │ │
│ │ │ Nginx:80 ──映射──► :3200 │ Express:3201 (仅内网) │ │ │
│ │ │ │ │ │ │ │
│ │ │ / → 静态页面 │ │ 健康检查: curl :3201/ │ │ │
│ │ │ /api/* ────────────┼───►│ 数据源: 和风天气 API │ │ │
│ │ │ proxy_pass │ │ │ │ │
│ │ │ backend:3201│ │ Volume: weather-data │ │ │
│ │ │ │ │ └─ /app/data/weather.db │ │ │
│ │ └─────────────────────┘ └─────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘请求链路总结:
浏览器 ──► ECS:80 (宿主 Nginx)
│
├─ weather.gouxinjie.com ──► 127.0.0.1:3200 (frontend 容器)
│ │
│ ├─ / → 返回静态资源
│ └─ /api/* ──► backend:3201 (Docker DNS)
│ │
│ ├─ 缓存/快照 命中 → 直接返回
│ └─ 未命中 → 和风天气 API → 写快照 → 返回
│
└─ 其他域名 ──► /var/www/blog (默认站点)2.2 端口规划
| 端口 | 归属 | 说明 |
|---|---|---|
| 80 | 宿主 Nginx | 对外唯一入口,按域名分流多项目 |
| 3200 | weather-frontend | Docker 宿主机映射,供宿主 Nginx 反代 |
| 3201 | weather-backend | 容器内网端口,仅 frontend 容器可访问 |
2.3 容器与网络
两个容器通过 docker-compose 加入 weather-net(bridge 网络),容器间使用服务名(如 backend)互访,借助 Docker 内置 DNS 解析。backend 配有健康检查(curl -f http://localhost:3201/),frontend 的 depends_on condition: service_healthy 确保后端就绪后再启动前端容器。
数据持久化通过 Docker Volume(weather-data)挂载到 backend 容器的 /app/data 目录,SQLite 数据库文件落盘不丢失。
2.4 CI/CD 流水线
项目使用 GitHub Actions 自动化部署,工作流文件位于 .github/workflows/deploy.yml。触发条件为 push main 分支,完整流程:
| # | 阶段 | 操作 |
|---|---|---|
| 1 | 构建 | GitHub Actions Runner 上 docker build 前端 + 后端镜像 |
| 2 | 导出 | docker save 导出 tar,gzip 压缩减小体积 |
| 3 | 上传 | SCP 上传镜像 tar.gz + docker-compose.yml 到 ECS |
| 4 | 加载 | ECS 端 docker load 载入镜像 |
| 5 | 切换 | docker compose down → docker compose up -d,滚动替换 |
| 6 | 校验 | 检查容器状态与 backend 健康检查结果 |
安全性要点:
- 和风天气 API Key 等敏感信息通过 GitHub Secrets 注入,部署时逐行写入 ECS 的
.env文件,不进入源码仓库。 - ECS 无需安装 Node.js 或编译工具链 — 构建在 GitHub Runner 上完成,ECS 仅需 Docker Engine。
三、部署遇到的问题与解决
本章记录从首次部署到稳定运行过程中遇到的 4 个关键问题及排查修复过程。
3.1 问题一:后端容器健康检查失败(unhealthy)
3.1.1 现象
首次执行 docker compose up -d 后,weather-frontend 一直处于 Waiting 状态,最终报错:
dependency failed to start: container weather-backend is unhealthydocker compose ps 显示 backend 的 STATUS 为 unhealthy。
3.1.2 排查
检查 docker-compose.yml 中 backend 的健康检查配置:
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3201/"]后端基础镜像为 node:20-alpine,其内置的 wget 来自 BusyBox,功能精简。BusyBox wget 不支持 GNU wget 的 --spider 和 --no-verbose 参数。因此健康检查命令在容器内实际执行失败,进程退出码非 0,Docker 判定为 unhealthy。
3.1.3 解决方案
两步修复:
在
server/Dockerfile中安装 curl(运行时已需要 python3/make/g++,追加 curl 即可):dockerfileapk add --no-cache python3 make g++ curl用
curl -f替换 wget 做健康检查 —curl -f会在 HTTP 状态码 ≥ 400 时返回非零退出码:dockerfileCMD curl -f http://localhost:3201/ || exit 1同时在
server/src/routes/index.ts补上GET /根路由,返回 200 供健康检查使用:tsrouter.get('/', (_req, res) => { res.json({ success: true, message: '服务正常运行' }); });
3.2 问题二:前端容器启动时 80 端口被占用
3.2.1 现象
后端健康检查通过后,frontend 容器启动失败:
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use原因:docker-compose.yml 中前端映射了 80:80,但 ECS 宿主 Nginx 已占用 80 端口(为 blog 等其他项目服务)。
3.2.2 错误尝试
最初尝试在 CI 脚本中添加 sudo fuser -k 80/tcp 强制释放 80 端口。但这样做会杀死宿主 Nginx,导致 ECS 上其他项目(如 blog)也一起掉线。这是不可接受的 — ECS 是多项目共享的,不能因为一个项目影响其他服务。
3.2.3 正确方案
将 weather-frontend 容器映射改为 3200:80,不再抢占宿主 80 端口。由宿主 Nginx 按 server_name 反代到 3200,与 blog 共用 80:
ports: # docker-compose.yml
- "3200:80" # 容器内 Nginx 监听 80,宿主机映射到 3200同时在 ECS 宿主 Nginx 的 /etc/nginx/conf.d/weather.conf 中新增:
server {
listen 80;
server_name weather.gouxinjie.com;
location / {
proxy_pass http://127.0.0.1:3200;
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;
}
}3.3 问题三:域名解析到错误的项目
3.3.1 现象
所有容器启动正常,但浏览器访问 http://weather.gouxinjie.com 后显示的是 blog 项目的页面,而非天气大屏。
3.3.2 根因分析
观察 ECS 宿主 Nginx 主配置文件 /etc/nginx/nginx.conf,其中只有一条 server 块且标记为 default_server:
server {
listen 80 default_server;
server_name _;
root /var/www/blog;
...
}server_name _ 会匹配所有未被其他 server_name 匹配的域名。而 weather.gouxinjie.com 当时还没有单独的 server 块,自然被 default_server 捕获,走到了 blog。
这是 Nginx 的预期行为 — 不是 bug,而是配置缺失。
3.3.3 修复
在 /etc/nginx/conf.d/weather.conf 中添加专门的反代配置后,Nginx 会按最长匹配优先规则,将 weather.gouxinjie.com 的请求路由到 127.0.0.1:3200,其余域名继续走 default_server → blog。
执行 nginx -t && nginx -s reload 使配置生效,无需重启 Nginx 主进程,不会影响已连接的 blog 用户。
3.4 问题四:前端容器 Nginx 健康检查潜在风险
前端容器的 Dockerfile 中同样使用了 wget 做健康检查:
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1前端基础镜像是 nginx:alpine(不是 node:alpine),其自带的 wget 也是 BusyBox 版本,同样不支持 --spider 和 --no-verbose。不过由于 Nginx 容器仅返回静态文件,健康检查用 wget 虽然参数不匹配但通常也能返回确认信号,因此这个问题影响较小。如需彻底修复,可以同样改为 curl 或使用更兼容的方式。
四、关键配置文件说明
4.1 docker-compose.yml(Docker 编排核心)
位置:项目根路径
定义两个服务容器 frontend 和 backend,共享 weather-net 网络和数据卷 weather-data。backend 通过环境变量注入和风天气配置,frontend 的 depends_on 依赖 backend 的健康检查。
生产环境启动命令:
cd /var/www/weather && docker compose up -d4.2 server/Dockerfile(后端镜像)
多阶段构建:builder 阶段安装编译工具链 + npm ci + tsc 编译;运行阶段仅保留生产依赖(npm ci --omit=dev)和编译产物 dist/。better-sqlite3 是原生 C++ 模块,两个阶段都需要 python3/make/g++。运行阶段额外安装 curl 用于健康检查。
4.3 Dockerfile(前端镜像,根目录)
多阶段构建:builder 阶段执行 npm run build(Vite)产生 dist/;运行阶段使用 nginx:alpine 作为静态服务器,将 dist/ 拷贝至 /usr/share/nginx/html。使用 deploy/nginx-frontend.conf 作为 Nginx 站点配置(包含 /api/ 反代到 backend:3201 和 SPA 回退规则)。
4.4 .env(生产环境变量)
由 GitHub Actions 在部署时动态生成,包含以下字段(不进入 Git):
| 环境变量 | 说明 |
|---|---|
QWEATHER_API_KEY | 和风天气 API Key |
QWEATHER_API_HOST | 和风天气专属 API Host(不含 https://) |
DEFAULT_LOCATION_ID | 默认城市 LocationID(默认 101020100 上海) |
DEFAULT_CITY_NAME | 默认城市名称(默认上海) |
LOG_LEVEL | 日志级别:info |
SNAPSHOT_RETENTION_DAYS | 快照数据保留天数:90 |
4.5 deploy/nginx-frontend.conf(前端容器内 Nginx 配置)
关键规则:
- Gzip 压缩已开启(comp_level 6)
/assets/长缓存(1 年 + immutable)/api/反向代理到backend:3201(Docker 服务发现)- SPA 回退:
try_files $uri $uri/ /index.html
4.6 宿主 Nginx 配置
位于 ECS 的 /etc/nginx/conf.d/weather.conf,负责将 weather.gouxinjie.com 流量转发到 127.0.0.1:3200。
注意:该配置不在项目源码中管理,需要手动或首次部署时在 ECS 上创建。后续 Nginx 配置变更需要 SSH 登录执行 nginx -s reload。
五、运维指南
5.1 日常命令
| 操作 | 命令 |
|---|---|
| 查看容器状态 | docker compose ps |
| 查看后端日志 | docker logs weather-backend --tail 50 |
| 查看前端日志 | docker logs weather-frontend --tail 50 |
| 重启服务 | docker compose restart |
| 重新部署 | docker compose down && docker compose up -d |
| 清理旧镜像(释放磁盘) | docker image prune -f |
| 进入后端容器 | docker exec -it weather-backend sh |
| 查看 SQLite 数据 | docker exec weather-backend ls /app/data/ |
| Nginx 重载配置 | nginx -t && nginx -s reload |
5.2 健康检查验证
部署完成后,通过以下方式验证服务正常:
检查容器健康状态应全部为 healthy:
bashdocker compose ps后端健康检查端点:
bashcurl http://127.0.0.1:3201/ # => { "success": true, "message": "服务正常运行" }前端页面可访问:
bashcurl -I http://127.0.0.1:3200/ # => HTTP/1.1 200 OK通过域名校验 Nginx 反向代理链路:
bashcurl -I http://weather.gouxinjie.com/
5.3 数据库备份
SQLite 数据库文件由 Docker Volume weather-data 持久化。备份方式:
# 方式一:直接复制数据库文件
docker cp weather-backend:/app/data/weather.db ./backup_$(date +%Y%m%d).db
# 方式二:进入容器执行 sqlite3 .dump
docker exec weather-backend sqlite3 /app/data/weather.db .dump > dump.sql5.4 紧急回滚
如果新版本部署后出现问题,可以手动切换到上一个版本:
- 在 GitHub Actions 找到最近一次成功的 workflow run
- 触发该 workflow 的 re-run(或 revert 代码后重新 push)
- GitHub Actions 会自动执行 docker build + deploy 流程,覆盖当前容器
注意:由于 Docker 镜像是每次构建产生的,ECS 上不保留历史镜像,因此回滚实际上是通过重新构建旧版本来实现的。
六、附录
6.1 首次部署 Checklist
以下清单适用于在新 ECS 上从零部署:
GitHub Actions Secrets 配置(步骤 3 的详细说明):
| Secret 名称 | 描述 | 示例值 |
|---|---|---|
ECS_SSH_PRIVATE_KEY | ECS 服务器 SSH 私钥,用于 GitHub Actions 免密登录 | -----BEGIN OPENSSH PRIVATE KEY-----... |
ECS_HOST | ECS 服务器公网 IP 或域名,部署目标地址 | 47.xx.xx.xx |
ECS_USER | ECS 服务器 SSH 登录用户名 | root |
QWEATHER_API_KEY | 和风天气 API Key,从和风天气控制台获取 | xxxxxxxxxxxxxxxxxxxxxxxx |
QWEATHER_API_HOST | 和风天气专属 API 域名(免费订阅为付费 Host),不含 https:// | api.qweather.com |
DEFAULT_LOCATION_ID | 默认城市 LocationID,定位失败时回退 | 101020100(上海) |
DEFAULT_CITY_NAME | 默认城市中文名,定位失败时回退 | 上海 |
配置路径:GitHub 仓库 → Settings → Secrets and variables → Actions → New repository secret
其余步骤:
- 安装 Docker Engine 和 Docker Compose
- 创建
/var/www/weather目录 - 配置以上 7 个 GitHub Actions Secrets
- 在 ECS 宿主添加
/etc/nginx/conf.d/weather.conf并执行nginx -s reload - DNS 将
weather.gouxinjie.com解析到 ECS IP - 触发一次 push 或 GitHub Actions 手动构建
- 确认
docker compose ps两个容器状态均为 healthy - 浏览器访问确认页面正常
6.2 和风天气 API 配额说明
本项目使用和风天气免费订阅,每天的 API 调用次数有限制。后端通过多层缓存策略控制调用频率:
- 内存缓存:实时天气 10 分钟 TTL,24h 趋势 30 分钟,7 天预报 60 分钟
- 数据库快照兜底:缓存失效时优先使用 SQLite 中最近一次快照
- 三层降级:内存缓存 → 数据库快照 → 空数据标记
这样的设计下,即使短时间内多次刷新页面,实际打到和风天气的请求也会被缓存拦截,有效保护免费额度。
6.3 项目仓库结构
weather-dashboard/
├── .github/workflows/deploy.yml # CI/CD 工作流
├── Dockerfile # 前端镜像(Vite + Nginx)
├── docker-compose.yml # 双容器编排
├── deploy/nginx-frontend.conf # 前端容器 Nginx 配置
├── server/ # 后端源码
│ ├── Dockerfile # 后端镜像(Express)
│ ├── .env.example # 环境变量模板
│ └── src/ # Express 源文件
├── src/ # 前端源码
├── docx/ # 项目文档
├── scripts/ # 辅助脚本
└── README.md6.4 参考链接
- 和风天气开发文档:https://dev.qweather.com/
- Docker Compose 文档:https://docs.docker.com/compose/
- Nginx 反向代理指南:https://nginx.org/en/docs/http/ngx_http_proxy_module.html
— 文档结束 —