React+Node+SQLite项目自动发布到ECS实战-git看板
更新: 7/3/2026字数: 0 字 时长: 0 分钟
CodeView项目部署实战:用 GitHub Actions + Docker Compose 把项目稳定发布到阿里云 ECS
一、项目背景
CodeView 是一个基于 GitHub 数据的可视化产品,目标不是做一个简单的项目列表页面,而是把 GitHub 仓库的活跃度、提交趋势、技术栈画像、流量数据和基础经营数据沉淀下来,形成一个长期可用的数据看板。
从技术形态看,它是一个标准的前后端分离项目:
- 前端:
React + Vite + TypeScript + SCSS + ECharts - 后端:
Node.js + Express + TypeScript - 数据库:
SQLite - 运行方式:前后端双容器部署
这类项目上线时,真正麻烦的部分通常不是“把程序跑起来”,而是如何把下面这些环节串成稳定链路:
- 前端静态资源发布
- 后端 API 服务启动
- SQLite 数据持久化
- 生产环境变量管理
- 自动化部署
- 镜像仓库管理
- 服务器目录和回滚策略
这篇文章记录的,就是 CodeView 从本地开发到阿里云 ECS 实际落地的一整套部署方案。
二、部署前的准备清单
在真正开始自动部署前,建议先把准备项一次性整理好。这样可以避免 workflow 第一次运行就因为环境不完整而失败。
1. GitHub 仓库侧
需要具备:
- 可正常 push 的 GitHub 仓库
- 已启用 GitHub Actions
- 仓库默认发布分支为
main
同时需要进入仓库:
Settings -> Secrets and variables -> Actions提前配置好部署所需 Secrets。
2. 阿里云侧
需要具备:
- 一台可 SSH 登录的 ECS
- 已创建好的阿里云 ACR 个人版实例
- 已创建好的 ACR 命名空间
- 已创建好的镜像仓库:
codeview/servercodeview/web
以当前项目为例,实际使用的是:
ACR_REGISTRY=crpi-********.cn-shanghai.personal.cr.aliyuncs.com
ACR_NAMESPACE=codeview3. ECS 安全组
安全组至少要放行这些端口:
22:用于 GitHub Actions SSH 登录服务器80:用于域名访问网站
如果你还没接入宿主机 Nginx,临时直接用公网端口访问容器,也可以临时放行:
81
但在当前最终方案里,如果已经由宿主机 Nginx 反代 127.0.0.1:81,那外网层面其实不一定需要长期暴露 81。
三、这次部署想解决什么问题
这次不是临时上线一个 demo,而是希望得到一套可长期维护的生产方案。
目标很明确:
- 推送到 GitHub
main后自动部署。 - ECS 只负责运行,不在服务器本地构建业务镜像。
- 配置和代码分离,避免每次发版都改环境文件。
- 保留最近版本,出现问题时能快速回滚。
- 尽量少做手工操作,降低后续维护成本。
最终落地后的核心链路是:
- GitHub Actions 构建镜像
- 推送镜像到阿里云 ACR
- ECS 通过 Docker Compose 拉取镜像并启动
- 宿主机 Nginx 保持在
80端口,对 CodeView 做反向代理 - ECS 内只保留最近两个 release
四、为什么没有直接在 ECS 上执行 docker compose up --build
一开始最直觉的方案其实很简单:
- GitHub Actions 通过 SSH 连到 ECS。
- 上传源码。
- 在 ECS 上执行
docker compose up -d --build。
看起来很合理,但实际部署时很快遇到三个关键问题。
1. Docker 服务没有启动
第一次运行工作流时,直接报错:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock这说明不是部署脚本先错了,而是 ECS 本身的 Docker 服务还没准备好。
2. ECS 无法稳定访问 Docker Hub
后面即使把 Docker 服务启动起来,构建阶段又卡在基础镜像拉取:
node:20-bookworm-slim: failed to resolve source metadata在 ECS 上手动执行:
docker pull node:20-bookworm-slim同样失败,报 context deadline exceeded 或 i/o timeout。
这说明问题不是 workflow 写错了,而是 ECS 到 Docker Hub 的访问本身不稳定。继续坚持“在 ECS 上构建业务镜像”没有意义。
3. 宿主机 80 端口已经被 Nginx 占用
当镜像问题绕过去后,容器启动又遇到端口冲突:
bind: address already in use排查后发现,是宿主机 Nginx 已经监听了 80 端口。
这也意味着容器不能直接把 Web 服务绑定到宿主机 80,必须重新设计入口层。
五、最终采用的部署方案
经过调整,最终方案变成下面这样:
核心原则
- 镜像只在 GitHub Actions 中构建
- 镜像统一推送到阿里云 ACR
- ECS 只负责拉镜像和运行容器
- 生产环境变量放在共享目录
- 当前版本通过固定软链接访问
- 自动清理旧版本,只保留最新两个 release
总体流程图
六、最终部署架构说明
1. 容器拆分
项目分成两个容器:
server负责 Node.js API、GitHub 数据同步、SQLite 读写和定时任务。web负责托管前端静态资源,并把/api转发到server:3101。
2. 目录结构
ECS 上目录设计如下:
/var/www/codeview/
releases/
<commit_sha_1>/
<commit_sha_2>/
shared/
.env
data/
current -> /var/www/codeview/releases/<latest_commit_sha>这套结构的好处很明显:
releases/保存发版快照,便于回滚。shared/.env独立保存生产配置,不跟着版本目录走。shared/data/存放 SQLite 数据,容器重建后数据还在。current是固定入口,手工维护时不用再查具体 commit 目录。
3. 域名与入口层
最终线上访问使用的是二级域名:
codeview.example.com但容器没有直接占用宿主机 80,而是采用下面这套入口结构:
简单说就是:
- 宿主机 Nginx 保留;
- CodeView 对外暴露到
81端口; - 宿主机 Nginx 再把域名请求转发到
127.0.0.1:81。
这能避免和宿主机已有 Web 服务抢占 80 端口。
七、为什么生产环境变量放在 shared/.env
这次部署中,一个非常关键的调整是:不使用 release 目录里的环境文件,而是统一使用共享环境文件。
最终路径为:
/var/www/codeview/shared/.env这样做比把环境文件放在每次发版目录里更合理,原因有三点:
- 代码和配置分离,发版时不需要重复修改配置。
- 版本切换时不影响环境变量。
- 后续修改端口、域名、镜像地址时,只改一份文件。
这也是为什么最终没有继续使用单独的 codeview.env 文件名,而是直接统一成 .env。这样和 Docker Compose 的习惯也一致。
八、生产环境的关键配置
1. GitHub Secrets
GitHub Actions 需要以下 Secrets:
ECS_HOSTECS_PORTECS_USERECS_SSH_KEYACR_REGISTRYACR_NAMESPACEACR_USERNAMEACR_PASSWORD
以当前项目为例:
ACR_REGISTRY=crpi-********.cn-shanghai.personal.cr.aliyuncs.com
ACR_NAMESPACE=codeview对应镜像地址为:
crpi-********.cn-shanghai.personal.cr.aliyuncs.com/codeview/server:latest
crpi-********.cn-shanghai.personal.cr.aliyuncs.com/codeview/web:latest2. ECS 共享环境文件
/var/www/codeview/shared/.env 可以按下面方式配置:
CODEVIEW_HTTP_PORT=81
CODEVIEW_DATA_DIR=/var/www/codeview/shared/data
SERVER_IMAGE=crpi-********.cn-shanghai.personal.cr.aliyuncs.com/codeview/server:latest
WEB_IMAGE=crpi-********.cn-shanghai.personal.cr.aliyuncs.com/codeview/web:latest
SERVER_PORT=3101
WEB_ORIGIN=http://codeview.example.com
DATABASE_PATH=/app/data/asset-console.db
DEFAULT_USER_ID=local-user
ENCRYPTION_SECRET=替换成足够长的随机字符串这里最关键的是两个变量:
CODEVIEW_HTTP_PORT
决定宿主机把哪个端口映射给容器内的 Web 服务。
因为宿主机 80 已被已有 Nginx 占用,所以实际值设为:
CODEVIEW_HTTP_PORT=81WEB_ORIGIN
这个值必须和外部用户实际访问地址一致,否则后端的跨域判断会出问题。
当前实际接入域名后,应写成:
WEB_ORIGIN=http://codeview.example.com如果没有走域名,而是直接通过公网 IP + 81 访问,则应改成:
WEB_ORIGIN=http://***.***.***.***:81九、GitHub 仓库里需要配置什么
1. workflow 文件
当前自动部署依赖仓库内这个文件:
.github/workflows/deploy-ecs-compose.yml它的触发方式是:
- push 到
main - 手动执行
workflow_dispatch
2. 必填 Secrets
GitHub Actions 至少需要下面这些 Secrets:
ECS_HOSTECS_PORTECS_USERECS_SSH_KEYACR_REGISTRYACR_NAMESPACEACR_USERNAMEACR_PASSWORD
可以按下面方式理解:
ECS_HOST/ECS_PORT/ECS_USER/ECS_SSH_KEY用于让 GitHub Actions 能登录你的 ECS。ACR_REGISTRY/ACR_NAMESPACE/ACR_USERNAME/ACR_PASSWORD用于让 GitHub Actions 能把镜像推送到阿里云 ACR,也让 ECS 能从 ACR 拉取镜像。
十、ECS 首次需要做哪些准备
如果是第一次部署到一台新的 ECS,建议先手工准备下面这些内容。
1. 安装并启动 Docker
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl status docker --no-pager2. 创建部署目录
sudo mkdir -p /var/www/codeview/releases
sudo mkdir -p /var/www/codeview/shared
sudo mkdir -p /var/www/codeview/shared/data3. 创建生产环境文件
sudo vi /var/www/codeview/shared/.env写入前面那份 .env 内容即可。
4. 确认 Docker Compose 可用
docker compose version5. 确认端口占用情况
sudo ss -lntp | grep ':80'
sudo ss -lntp | grep ':81'如果 80 已被宿主机 Nginx 占用,这是正常现象,只要 81 没冲突即可。
十一、GitHub Actions 的实际部署逻辑
当前这套 workflow 的实际动作可以概括为:
- 拉取仓库代码。
- 执行
npm ci。 - 执行
npm run typecheck。 - 构建
server镜像。 - 构建
web镜像。 - 推送镜像到阿里云 ACR。
- 使用
git archive打包当前提交的源码。 - 上传压缩包到 ECS。
- 在 ECS 解压到
/var/www/codeview/releases/<commit_sha>。 - 更新
/var/www/codeview/current软链接。 - 执行
docker compose pull。 - 执行
docker compose up -d。 - 自动清理旧版本,只保留最近两个 release。
这套逻辑和“在 ECS 本地 build 镜像”最大的区别在于:
- ECS 不再承担构建职责;
- ECS 只做稳定的拉取和运行;
- 构建失败会集中发生在 GitHub Actions 上,更容易排查。
十二、部署时踩过的坑,以及为什么这样修
这一部分最有参考价值,因为它直接决定了最终方案为什么是现在这样。
坑 1:Cannot connect to the Docker daemon
根因:
- Docker 服务没有启动。
修复:
sudo systemctl enable docker
sudo systemctl start docker经验:
- 先确认服务器运行环境,再谈自动化部署。
- 首次上 ECS,一定先手工验证 Docker 是否可用。
坑 2:docker pull node:20-bookworm-slim 超时
根因:
- ECS 访问 Docker Hub 不稳定。
- 用 ECS 充当镜像构建机不合适。
修复思路:
- 不再在 ECS 上执行
docker build; - 改成 GitHub Actions 构建后推送到 ACR。
经验:
- 构建和运行应该分离。
- 服务器越简单,生产环境越稳定。
坑 3:Web 容器绑定 80 失败
根因:
- 宿主机 Nginx 已经占用
80。
排查命令:
sudo ss -lntp | grep ':80'修复:
- 保留宿主机 Nginx;
- 把 CodeView 的容器端口改为
81; - 由宿主机 Nginx 转发到
127.0.0.1:81。
经验:
- 端口冲突不是单纯“谁抢到谁用”的问题;
- 生产环境更重要的是入口层边界清晰。
坑 4:每次手工命令都要写完整 release 目录
原来重启要写成这样:
docker compose --project-name codeview --file /var/www/codeview/releases/<commit_sha>/compose.yaml --env-file /var/www/codeview/shared/.env up -d太长,也容易输错。
修复:
- 增加
/var/www/codeview/current软链接。
修复后命令固定成:
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env up -d经验:
- 生产环境里,固定入口路径是非常划算的设计。
坑 5:releases/ 会越来越大
根因:
- 每次发布都会新生成一个 release 目录。
修复:
- 在 workflow 中自动清理旧版本。
- 只保留最新两个 release。
经验:
- 版本快照要有,但不能无限堆积。
- “当前版本 + 上一个版本”通常已经足够满足回滚需求。
十三、域名解析和宿主机 Nginx 还要做什么
如果你要通过 codeview.example.com 访问,而不是直接通过 IP:81,还需要补两步。
1. 配置 DNS 解析
在你的域名 DNS 控制台中,给二级域名增加一条 A 记录:
主机记录:codeview
记录类型:A
记录值:你的 ECS 公网 IP这样 codeview.example.com 才会解析到 ECS。
2. 配置宿主机 Nginx
因为当前方案保留了宿主机 Nginx,所以域名入口应该配置在宿主机,而不是容器外再暴露一个公网 80。
一个常见写法如下:
server {
listen 80;
server_name codeview.example.com;
location / {
proxy_pass http://127.0.0.1:81;
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;
}
}这里要注意两件事:
deploy/nginx/default.conf是容器内 Nginx 配置,不是宿主机 Nginx 配置。- 如果你保留宿主机 Nginx,那宿主机反向代理配置仍然需要你自己维护。
也就是说:
- 容器内
default.conf不需要手工拷贝; - 宿主机上的反向代理规则,需要按你的域名和服务器情况配置。
十四、宿主机 Nginx 配完后怎么生效
配置文件写完后,建议在 ECS 上执行:
sudo nginx -t
sudo systemctl reload nginx如果 nginx -t 失败,先不要 reload,先把语法错误修掉。
十五、如何验收部署是否成功
部署完成后,可以按下面顺序检查。
1. 查看容器状态
docker ps预期至少能看到:
codeview-servercodeview-web
2. 验证容器端口
curl http://127.0.0.1:81/api/health预期返回类似:
{"success":true,"code":200,"message":"操作成功","data":{"status":"ok"}}3. 查看 compose 状态
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env ps4. 查看最近日志
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env logs --tail=1005. 浏览器访问
如果走宿主机端口访问,可以先试:
http://***.***.***.***:81/接入域名并配置好宿主机 Nginx 后,再访问:
http://codeview.example.com十六、出问题时如何回滚
因为当前只保留最近两个 release,所以回滚也比较直接。
1. 先查看现有 release
ls -lt /var/www/codeview/releases2. 把 current 指向上一个版本
假设你要回滚到:
/var/www/codeview/releases/<previous_commit_sha>则执行:
ln -sfn /var/www/codeview/releases/<previous_commit_sha> /var/www/codeview/current3. 重新启动 compose
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env up -d这套方式的前提是:
- 上一个 release 目录还在
- 镜像 tag 仍然可拉取
- 共享配置和数据目录没有被破坏
十七、手工运维时最常用的命令
查看运行状态
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env ps重新拉取并启动
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env pull
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env up -d查看日志
docker compose --project-name codeview --file /var/www/codeview/current/compose.yaml --env-file /var/www/codeview/shared/.env logs --tail=100查看当前 release 指向
ls -l /var/www/codeview/current查看已有版本
ls -lt /var/www/codeview/releases十八、这套方案适合什么项目
这套方案很适合下面几类项目:
- 个人项目
- 小型管理后台
- 前后端分离项目
- 使用 SQLite 或本地文件持久化的项目
- 单台 ECS 即可承载的业务
它的优点是很务实:
- 自动化足够完整;
- 结构足够清晰;
- 运维复杂度不高;
- 已经具备基础回滚能力;
- 对个人开发者和小团队非常友好。
如果后续项目继续演进到下面这些阶段:
- 多实例部署
- 多服务拆分
- 云数据库替代 SQLite
- 灰度发布、弹性扩缩容、服务治理
那时再考虑升级到 Kubernetes 或托管容器平台会更合适。
十九、结论
这次 CodeView 上线,真正有价值的不是“项目终于能访问了”,而是把部署这件事做成了可持续维护的工程方案。
最终沉淀下来的关键设计有五个:
- GitHub Actions 负责构建。
- 阿里云 ACR 负责托管镜像。
- ECS 只负责拉镜像和运行。
shared/.env负责统一生产配置。current + releases负责稳定运维和基础回滚。
从结果上看,这套方案已经满足一个真实线上项目最关键的要求:
- 能自动部署
- 能稳定运行
- 能快速排查
- 能保留版本
- 能继续演进
如果后面继续完善,最值得追加的方向通常是:
- 给域名接入 HTTPS。
- 增加容器健康检查。
- 补充日志归档和监控。
- 为 SQLite 做备份策略。
但对于当前阶段的 CodeView 来说,这套方案已经足够专业,也足够简单易懂,完全可以作为个人项目上云的标准实践。