Skip to content
 

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.tsx

1.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:3601api 通过内部网桥访问 db

1.3 关键取舍

  • 前后端同源:前端接口写死相对路径 /api,鉴权依赖 refresh_token(HttpOnly) + csrf_token Cookie。同源部署下纯 HTTP 才能正常保存 Cookie。
  • 镜像在 GitHub Actions 构建、ECS 只 pull:避免小机器跑 vite build OOM、避免 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 进容器 803610 不是服务,只是把流量递进容器的通道。

以访问首页为例逐跳(右侧为对应代码):

发生位置对应代码
① 浏览器chat.gouxinjie.com 解析到 ECS 公网 IP,访问 :80
② 宿主机 nginx 接 80server_name 命中,监听 80deploy/server_chat.conf:8,10
②→③ 转发proxy_pass http://127.0.0.1:3610deploy/server_chat.conf:19
③ Docker 映射宿主机 3610 NAT 进容器 80deploy/docker-compose.yml:60-61
④ 容器内 nginx 接 80镜像 nginx:1.27-alpinelisten 80frontend/Dockerfile:22 / frontend/deploy/nginx.conf:8
④ 静态 / SPA 回退try_files $uri $uri/ /index.htmlfrontend/deploy/nginx.conf:55-57
/api 反代proxy_pass http://api:3601frontend/deploy/nginx.conf:36-37
⑤ api 容器uvicorn 监听 0.0.0.0:3601deploy/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-slimbackend/Dockerfile:6),启动命令 uvicorn main:app --host 0.0.0.0 --port 3601backend/Dockerfile:28-30)。必须监听 0.0.0.0 且开 --proxy-headers 才能被容器网桥连入、拿到真实客户端 IP。端口 3601 是容器内部端口,不暴露到宿主机。

3.2 frontend(web 容器)

React 编译后的静态资源由容器内的 nginx 托管(并非 Node 运行),镜像基于 nginx:1.27-alpinefrontend/Dockerfile:22),监听 80frontend/deploy/nginx.conf:8),负责静态托管、SPA 回退与 /api 反代;容器 80 经 compose 映射到宿主机 3610deploy/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(个人版,免费)

  1. 控制台 → 容器镜像服务 → 个人实例 → 开通(免费)。
  2. 左侧「访问凭证」→ 设置固定密码(此即 ACR_PASSWORD;无法查看,只能重置)。
  3. 左侧「仓库管理 → 命名空间」→ 创建 gouxinjie,建议设为私有并开启「自动创建仓库」。
  4. 确认 Registry 专属域名(上海形如 crpi-xxxxxxxx.cn-shanghai.personal.cr.aliyuncs.com)。同地域 ECS 可改用控制台「访问凭证」页的 VPC 内网地址(含 -vpc.)拉取,走内网不计公网流量。
  5. 开启「自动创建仓库」后,首次 docker push 会自动建 deepxinjie-api / deepxinjie-web

5.2 域名与安全组

  • DNS:给 gouxinjie.com 添加 A 记录 chat → ECS 公网 IP。
  • 安全组:放行 80(SSH 22 通常已放行)。

5.3 部署路径

统一使用 /var/www/chat(compose 内 ./schema.sql./.env 均相对该目录)。

六、GitHub Secrets 配置

仓库 → Settings → Secrets and variables → Actions → 逐个添加:

Secret取值
ACR_USERNAME阿里云账号全名(如 aliyun4356291210
ACR_PASSWORDACR 固定密码(见 5.1)
ECS_HOSTECS 公网 IP
ECS_USER登录用户,如 root
ECS_PORT22
ECS_SSH_KEY私钥全文(含 -----BEGIN... / -----END... 及所有换行)

NAMESPACE=gouxinjieREGISTRY 已写死在 deploy.yml.env.example 中,无需建 Secret。

SSH 密钥生成(本地):ssh-keygen -t ed25519 -C "github-actions" -f ./gha_deploy -N "",把 gha_deploy.pub 追加到服务器 ~/.ssh/authorized_keysgha_deploy 全文填入 ECS_SSH_KEY

七、服务器准备(一次性)

bash
# 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 填写,必填项务必填真实值):

dotenv
# 镜像仓库(上海地域公网地址;同地域可改 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

  1. build:以仓库根为 context,分别用 backend/Dockerfilefrontend/Dockerfile 构建并推送 deepxinjie-api / deepxinjie-web(tag 为 commit sha 与 latest,GHA 缓存按 scope=api / scope=web 隔离)。
  2. deploy
    • applesoft/scp-actiondeploy/docker-compose.ymlbackend/schema.sql 同步到 /var/www/chat(strip 一层)。
    • applesoft/ssh-action 校验 .env 存在 → 写入 IMAGE_TAGdocker logindocker compose pulldocker compose up -d --remove-orphans → 清理悬空镜像。
  3. 健康检查:重试 10 次 curl http://127.0.0.1:3610/api/hello,全部失败则报错退出。

手动触发:Actions 页对 Build & Deploy to ECSRun workflow

九、数据库初始化与演示账号

db 首次启动(数据卷为空)自动执行挂载的 schema.sql 建表;api 容器启动阶段也会 initialize_*_schema() 兜底建表。

建演示账号:

bash
cd /var/www/chat
docker compose exec api python seed_user.py
# 输出:体验用户 13113183859 已创建,密码为 123456。

日常运维:

bash
docker compose ps                       # 查看三个容器状态
docker compose logs -f api             # 跟踪后端日志
docker compose exec api python init_db.py   # 手动初始化/修复表结构

数据持久化在具名卷 mysql_datadocker compose down 不会删除;切勿使用 down -v

十、验证

bash
# 服务器本地
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.tsgenerateUUID()(优先原生、降级 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_keyschmod 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=trueSameSite=none 会让浏览器丢弃 refresh_token / csrf_token
  • 修复.env 必须 COOKIE_SECURE=falseCOOKIE_SAMESITE=lax

十二、升级到 HTTPS(推荐)

纯 HTTP 下 crypto.subtle 等 API 不可用、Cookie 安全性弱。建议尽早启用 HTTPS:

  1. 申请证书(阿里云免费 DV,或 Let's Encrypt / certbot)。
  2. 宿主机 nginx 增加 listen 443 ssl; 与证书配置,原 80 段 return 301 https://$host$request_uri;
  3. 服务器 /var/www/chat/.env 修改:COOKIE_SECURE=trueCORS_ORIGINS=https://chat.gouxinjie.com
  4. 容器、compose、Actions 无需改动,重新部署一次即可。

十三、数据备份

MySQL 数据在具名卷 mysql_data。建议加 crontab 定时备份:

bash
# 每日 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,回滚只需把 .envIMAGE_TAG 改回旧 sha 并执行:

bash
cd /var/www/chat
sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=<旧commit_sha>|" .env
docker compose pull && docker compose up -d