React+Python+Mysql+Docker自动部署到ECS实战
更新: 8/1/2026字数: 0 字 时长: 0 分钟
本文档记录 DeepXinjie(前后端分离 AI 聊天应用)在阿里云 ECS 上的生产部署方案:
GitHub Actions 自动构建镜像 → 推送阿里云 ACR → SSH 登录 ECS 拉取并重启,对外通过二级域名 chat.gouxinjie.com 提供纯 HTTP 访问。
一、项目构成与部署架构
1.1 项目构成
DeepXinjie 是前后端分离的 AI 聊天应用,由四部分组成:
- 前端:React 19 + TypeScript + Vite + Zustand,源码在
frontend/src,构建产物为静态资源dist/。 - 后端:FastAPI + Uvicorn,路由在
backend/routers、业务在backend/services,统一对外提供/api。 - 数据库:MySQL 8,表结构见
backend/schema.sql。 - 部署:两个 Dockerfile(
backend/、frontend/)+docker-compose.yml编排 + 阿里云 ACR 存镜像 + GitHub Actions 自动发布。
项目文件夹结构:
├── backend
│ ├── Dockerfile
│ ├── __init__.py
│ ├── main.py
│ ├── schema.sql
│ └── routers
│ ├── __init__.py
│ ├── auth.py
│ ├── chat.py
│ └── user.py
├── frontend
│ ├── Dockerfile
│ ├── deploy
│ │ └── nginx.conf
│ └── src
│ ├── App.tsx1.2 部署架构图
公网用户
│ http://chat.gouxinjie.com:80
▼
┌───────────────────────────────────────────────────────────┐
│ ECS 宿主机 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 宿主机 nginx(占用 80) ← server_chat.conf │ │
│ │ chat.gouxinjie.com → proxy_pass 127.0.0.1:3610 │ │
│ └─────────────────────────┬───────────────────────┘ │
│ │ 127.0.0.1:3610 │
│ ▼ Docker 端口映射 (NAT) │
│ ┌─────────────────────────┴───────────────────────┐ │
│ │ Docker 网络 deepxinjie │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ web 容器 │ │ api 容器 │ │ │
│ │ │ nginx:alpine │──/api──►│ FastAPI │ │ │
│ │ │ :80 → 3610 │ │ :3601 │ │ │
│ │ │ 静态+SPA回退 │ └──────┬───────┘ │ │
│ │ └──────────────┘ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ db 容器 │ │ │
│ │ │ mysql:8.0 │ │ │
│ │ │ 卷 mysql_data │ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘说明:公网流量先到宿主机 nginx 的 80,转发至宿主机回环 3610;Docker 把 3610 NAT 进 web 容器的 80;web 容器内 nginx 再把 /api/* 反代到 api:3601,api 通过内部网桥访问 db。
1.3 关键取舍
- 前后端同源:前端接口写死相对路径
/api,鉴权依赖refresh_token(HttpOnly) +csrf_tokenCookie。同源部署下纯 HTTP 才能正常保存 Cookie。 - 镜像在 GitHub Actions 构建、ECS 只 pull:避免小机器跑
vite buildOOM、避免 ECS 直连 Docker Hub 拉取慢。 - 宿主机已有 nginx 占用 80:web 容器仅绑
127.0.0.1:3610,由宿主机 nginx 转发,互不冲突;db不映射 3306,避免与宿主机 MySQL 冲突。
二、请求链路追踪
ECS 上有两套网络栈:宿主机的(公网 IP、127.0.0.1)和 Docker 容器的(隔离私网)。ports: "127.0.0.1:3610:80" 在宿主机回环开了个转发窗口——发往宿主机 127.0.0.1:3610 的包被 Docker NAT 进容器 80。3610 不是服务,只是把流量递进容器的通道。
以访问首页为例逐跳(右侧为对应代码):
| 跳 | 发生位置 | 对应代码 |
|---|---|---|
| ① 浏览器 | chat.gouxinjie.com 解析到 ECS 公网 IP,访问 :80 | — |
| ② 宿主机 nginx 接 80 | server_name 命中,监听 80 | deploy/server_chat.conf:8,10 |
| ②→③ 转发 | proxy_pass http://127.0.0.1:3610 | deploy/server_chat.conf:19 |
| ③ Docker 映射 | 宿主机 3610 NAT 进容器 80 | deploy/docker-compose.yml:60-61 |
| ④ 容器内 nginx 接 80 | 镜像 nginx:1.27-alpine,listen 80 | frontend/Dockerfile:22 / frontend/deploy/nginx.conf:8 |
| ④ 静态 / SPA 回退 | try_files $uri $uri/ /index.html | frontend/deploy/nginx.conf:55-57 |
④ /api 反代 | proxy_pass http://api:3601 | frontend/deploy/nginx.conf:36-37 |
| ⑤ api 容器 | uvicorn 监听 0.0.0.0:3601 | deploy/docker-compose.yml:47-48 / backend/Dockerfile |
| ⑥ 响应原路返回 | 容器 80 → 宿主机 3610 → 宿主机 nginx:80 → 浏览器 | — |
关键结论:
- 不能直接 浏览器 → 容器:容器 IP 是私网,公网不可达;必须经过宿主机 nginx 这道公开入口。
- 为何是 3610:容器 80 映射到宿主机 3610(compose 决定),且只绑
127.0.0.1,公网摸不到,必须先过宿主机 nginx。 - 两层 nginx 分工:宿主机
server_chat.conf只做「80 → 3610」入口转发;容器内nginx.conf做静态托管 + SPA 回退 +/api反代。两层/api、/都须proxy_buffering off,否则流式对话不会逐字输出(见 十一、4)。
三、服务构成(简要)
3.1 backend(api 容器)
FastAPI + Uvicorn,镜像基于 python:3.11-slim(backend/Dockerfile:6),启动命令 uvicorn main:app --host 0.0.0.0 --port 3601(backend/Dockerfile:28-30)。必须监听 0.0.0.0 且开 --proxy-headers 才能被容器网桥连入、拿到真实客户端 IP。端口 3601 是容器内部端口,不暴露到宿主机。
3.2 frontend(web 容器)
React 编译后的静态资源由容器内的 nginx 托管(并非 Node 运行),镜像基于 nginx:1.27-alpine(frontend/Dockerfile:22),监听 80(frontend/deploy/nginx.conf:8),负责静态托管、SPA 回退与 /api 反代;容器 80 经 compose 映射到宿主机 3610(deploy/docker-compose.yml:60-61)。
四、部署文件清单
| 文件 | 作用 |
|---|---|
.dockerignore | 排除 .env / node_modules / dist / __pycache__,避免敏感配置与构建产物进镜像 |
.github/workflows/deploy.yml | 构建前后端镜像 → 推送 ACR → SSH 同步编排文件并拉取重启 → 健康检查 |
backend/Dockerfile | 后端镜像:python:3.11-slim + uvicorn 监听 0.0.0.0:3601 |
frontend/Dockerfile | 前端多阶段:node:22-alpine 构建 → nginx:1.27-alpine 托管静态资源 + 反代 /api |
frontend/deploy/nginx.conf | 容器内 nginx:SPA 回退 + /api 反代 + SSE 关缓冲 |
deploy/docker-compose.yml | 生产编排:web(127.0.0.1:3610) + api + mysql8,目录 /var/www/chat |
deploy/.env.example | 服务器环境变量模板(不含真实密钥) |
deploy/server_chat.conf | 宿主机 nginx 站点配置样例,部署到 /etc/nginx/conf.d/server_chat.conf |
五、前置准备(一次性)
5.1 阿里云 ACR(个人版,免费)
- 控制台 → 容器镜像服务 → 个人实例 → 开通(免费)。
- 左侧「访问凭证」→ 设置固定密码(此即
ACR_PASSWORD;无法查看,只能重置)。 - 左侧「仓库管理 → 命名空间」→ 创建
gouxinjie,建议设为私有并开启「自动创建仓库」。 - 确认 Registry 专属域名(上海形如
crpi-xxxxxxxx.cn-shanghai.personal.cr.aliyuncs.com)。同地域 ECS 可改用控制台「访问凭证」页的 VPC 内网地址(含-vpc.)拉取,走内网不计公网流量。 - 开启「自动创建仓库」后,首次
docker push会自动建deepxinjie-api/deepxinjie-web。
5.2 域名与安全组
- DNS:给
gouxinjie.com添加 A 记录chat→ ECS 公网 IP。 - 安全组:放行
80(SSH22通常已放行)。
5.3 部署路径
统一使用 /var/www/chat(compose 内 ./schema.sql、./.env 均相对该目录)。
六、GitHub Secrets 配置
仓库 → Settings → Secrets and variables → Actions → 逐个添加:
| Secret | 取值 |
|---|---|
ACR_USERNAME | 阿里云账号全名(如 aliyun4356291210) |
ACR_PASSWORD | ACR 固定密码(见 5.1) |
ECS_HOST | ECS 公网 IP |
ECS_USER | 登录用户,如 root |
ECS_PORT | 22 |
ECS_SSH_KEY | 私钥全文(含 -----BEGIN... / -----END... 及所有换行) |
NAMESPACE=gouxinjie 与 REGISTRY 已写死在 deploy.yml 与 .env.example 中,无需建 Secret。
SSH 密钥生成(本地):
ssh-keygen -t ed25519 -C "github-actions" -f ./gha_deploy -N "",把gha_deploy.pub追加到服务器~/.ssh/authorized_keys,gha_deploy全文填入ECS_SSH_KEY。
七、服务器准备(一次性)
# 1) 部署目录与环境变量
mkdir -p /var/www/chat && cd /var/www/chat
openssl rand -hex 32 # 复制输出,作为 JWT_SECRET
vim .env # 按下方模板填写
chmod 600 .env
# 2) 宿主机 nginx 站点(内容取 deploy/server_chat.conf)
vim /etc/nginx/conf.d/server_chat.conf
nginx -t && nginx -s reload
# 3) 确认 docker compose v2 可用
docker compose version.env 内容(按 deploy/.env.example 填写,必填项务必填真实值):
# 镜像仓库(上海地域公网地址;同地域可改 VPC 内网地址加速拉取)
REGISTRY=crpi-5ue84w8rjgqxg0s0.cn-shanghai.personal.cr.aliyuncs.com
NAMESPACE=gouxinjie
IMAGE_TAG=latest
# 数据库(容器内网,主机名固定为 db)
DB_HOST=db
DB_USER=root
DB_PASSWORD=<自定义强密码>
DB_NAME=chat_platform
# 鉴权
JWT_SECRET=<openssl rand -hex 32 生成的字符串>
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7
# 纯 HTTP:Secure 必须为 false,否则浏览器不保存 Cookie 导致登录态丢失
COOKIE_SECURE=false
COOKIE_SAMESITE=lax
# 同源部署,仅作兜底
CORS_ORIGINS=http://chat.gouxinjie.com
# 模型与联网搜索
DEEPSEEK_API_KEY=<你的 key>
DEEPSEEK_BASE_URL=https://api.deepseek.com
SEARCH_PROVIDER=
TAVILY_API_KEY=注意:
deploy/.env.example会被 git 跟踪;服务器上的.env由人工维护,禁止写入任何真实密钥到仓库。
八、自动发布
推送到 main 分支即触发 .github/workflows/deploy.yml:
- build:以仓库根为 context,分别用
backend/Dockerfile、frontend/Dockerfile构建并推送deepxinjie-api/deepxinjie-web(tag 为 commit sha 与latest,GHA 缓存按scope=api/scope=web隔离)。 - deploy:
applesoft/scp-action把deploy/docker-compose.yml、backend/schema.sql同步到/var/www/chat(strip 一层)。applesoft/ssh-action校验.env存在 → 写入IMAGE_TAG→docker login→docker compose pull→docker compose up -d --remove-orphans→ 清理悬空镜像。
- 健康检查:重试 10 次
curl http://127.0.0.1:3610/api/hello,全部失败则报错退出。
手动触发:Actions 页对 Build & Deploy to ECS 点 Run workflow。
九、数据库初始化与演示账号
db 首次启动(数据卷为空)自动执行挂载的 schema.sql 建表;api 容器启动阶段也会 initialize_*_schema() 兜底建表。
建演示账号:
cd /var/www/chat
docker compose exec api python seed_user.py
# 输出:体验用户 13113183859 已创建,密码为 123456。日常运维:
docker compose ps # 查看三个容器状态
docker compose logs -f api # 跟踪后端日志
docker compose exec api python init_db.py # 手动初始化/修复表结构数据持久化在具名卷
mysql_data,docker compose down不会删除;切勿使用down -v。
十、验证
# 服务器本地
curl http://127.0.0.1:3610/api/hello # 返回 Hello from my-deepseek backend
docker compose ps # web / api / db 均为 Up
# 浏览器
# 打开 http://chat.gouxinjie.com,用 13113183859 / 123456 登录
# 验证:流式对话逐字输出;登录后刷新页面不丢登录态十一、常见问题与踩坑记录
11.1 crypto.randomUUID is not a function
- 现象:发消息时前端抛
TypeError: crypto.randomUUID is not a function。 - 原因:
crypto.randomUUID仅在安全上下文(HTTPS / localhost / 127.0.0.1)可用。纯 HTTP 的chat.gouxinjie.com被判定为非安全上下文,该 API 为undefined。 - 修复:新增
frontend/src/utils/uuid.ts的generateUUID()(优先原生、降级Math.random),替换ChatMain.tsx两处调用。注意crypto.subtle等同样要求安全上下文,终极解法见 十二、HTTPS。
11.2 密码哈希崩溃 passlib / bcrypt 不兼容
- 现象:
seed_user.py或注册/登录报AttributeError: module 'bcrypt' has no attribute '__about__'或ValueError: password cannot be longer than 72 bytes。 - 原因:
passlib[bcrypt]拉到bcrypt>=4.1,与passlib 0.8.1不兼容。 - 修复:
backend/requirements.txt固定bcrypt==4.0.1。此 bug 不影响/api/hello健康检查(容器仍Up),但真实注册/登录会 500,必须重建 api 镜像。
11.3 SSH 认证失败 unable to authenticate
- 现象:Actions 的 scp/ssh 报
ssh: handshake failed ... attempted methods [none publickey]。 - 原因:
ECS_SSH_KEY内容损坏(粘贴丢换行/缺首尾行),或服务器~/.ssh/authorized_keys没有对应公钥,或权限不对。 - 排查:本地
ssh -i gha_deploy root@<IP> -p 22 echo SSH_OK验证;不通则把公钥追加到/root/.ssh/authorized_keys并chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys。
11.4 流式对话「一次性吐出全部」
- 原因:nginx 缓冲未关闭。
- 修复:宿主机
server_chat.conf与容器frontend/deploy/nginx.conf的/api/、/都须proxy_buffering off; proxy_cache off;(已配置)。两层任一层漏配都会失效。
11.5 登录后刷新即掉线 / POST 报 CSRF 失败
- 原因:纯 HTTP 下
COOKIE_SECURE=true或SameSite=none会让浏览器丢弃refresh_token/csrf_token。 - 修复:
.env必须COOKIE_SECURE=false、COOKIE_SAMESITE=lax。
十二、升级到 HTTPS(推荐)
纯 HTTP 下 crypto.subtle 等 API 不可用、Cookie 安全性弱。建议尽早启用 HTTPS:
- 申请证书(阿里云免费 DV,或 Let's Encrypt / certbot)。
- 宿主机 nginx 增加
listen 443 ssl;与证书配置,原 80 段return 301 https://$host$request_uri;。 - 服务器
/var/www/chat/.env修改:COOKIE_SECURE=true、CORS_ORIGINS=https://chat.gouxinjie.com。 - 容器、compose、Actions 无需改动,重新部署一次即可。
十三、数据备份
MySQL 数据在具名卷 mysql_data。建议加 crontab 定时备份:
# 每日 03:00 备份到 /var/backups(勿放入 /var/www/chat 以免被覆盖)
0 3 * * * docker compose -f /var/www/chat/docker-compose.yml exec -T db \
mysqldump -uroot -p"$DB_PASSWORD" chat_platform | gzip > /var/backups/chat_$(date +\%F).sql.gz十四、回滚
每次发布镜像 tag 为 commit sha,回滚只需把 .env 的 IMAGE_TAG 改回旧 sha 并执行:
cd /var/www/chat
sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=<旧commit_sha>|" .env
docker compose pull && docker compose up -d