Skip to content
 

城市环境与天气大屏 — 部署运维文档

更新: 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    │   │ │
│  │  └─────────────────────┘    └─────────────────────────────┘   │ │
│  └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

请求链路总结:

text
浏览器 ──► 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对外唯一入口,按域名分流多项目
3200weather-frontendDocker 宿主机映射,供宿主 Nginx 反代
3201weather-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 unhealthy

docker compose ps 显示 backend 的 STATUS 为 unhealthy。

3.1.2 排查

检查 docker-compose.yml 中 backend 的健康检查配置:

yaml
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 解决方案

两步修复:

  1. server/Dockerfile 中安装 curl(运行时已需要 python3/make/g++,追加 curl 即可):

    dockerfile
    apk add --no-cache python3 make g++ curl
  2. curl -f 替换 wget 做健康检查 — curl -f 会在 HTTP 状态码 ≥ 400 时返回非零退出码:

    dockerfile
    CMD curl -f http://localhost:3201/ || exit 1
  3. 同时在 server/src/routes/index.ts 补上 GET / 根路由,返回 200 供健康检查使用:

    ts
    router.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:

yaml
ports:            # docker-compose.yml
  - "3200:80"   # 容器内 Nginx 监听 80,宿主机映射到 3200

同时在 ECS 宿主 Nginx 的 /etc/nginx/conf.d/weather.conf 中新增:

nginx
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

nginx
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 做健康检查:

dockerfile
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 的健康检查。

生产环境启动命令:

bash
cd /var/www/weather && docker compose up -d

4.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 健康检查验证

部署完成后,通过以下方式验证服务正常:

  1. 检查容器健康状态应全部为 healthy:

    bash
    docker compose ps
  2. 后端健康检查端点:

    bash
    curl http://127.0.0.1:3201/
    # => { "success": true, "message": "服务正常运行" }
  3. 前端页面可访问:

    bash
    curl -I http://127.0.0.1:3200/
    # => HTTP/1.1 200 OK
  4. 通过域名校验 Nginx 反向代理链路:

    bash
    curl -I http://weather.gouxinjie.com/

5.3 数据库备份

SQLite 数据库文件由 Docker Volume weather-data 持久化。备份方式:

bash
# 方式一:直接复制数据库文件
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.sql

5.4 紧急回滚

如果新版本部署后出现问题,可以手动切换到上一个版本:

  1. 在 GitHub Actions 找到最近一次成功的 workflow run
  2. 触发该 workflow 的 re-run(或 revert 代码后重新 push)
  3. GitHub Actions 会自动执行 docker build + deploy 流程,覆盖当前容器

注意:由于 Docker 镜像是每次构建产生的,ECS 上不保留历史镜像,因此回滚实际上是通过重新构建旧版本来实现的。

六、附录

6.1 首次部署 Checklist

以下清单适用于在新 ECS 上从零部署:

GitHub Actions Secrets 配置(步骤 3 的详细说明):

Secret 名称描述示例值
ECS_SSH_PRIVATE_KEYECS 服务器 SSH 私钥,用于 GitHub Actions 免密登录-----BEGIN OPENSSH PRIVATE KEY-----...
ECS_HOSTECS 服务器公网 IP 或域名,部署目标地址47.xx.xx.xx
ECS_USERECS 服务器 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.md

6.4 参考链接

— 文档结束 —

我见青山多妩媚,料青山见我应如是