diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md new file mode 100644 index 0000000..e29512b --- /dev/null +++ b/DOCKER_DEPLOY.md @@ -0,0 +1,268 @@ +# Docker 部署说明 + +本文档说明如何在已有 Docker nginx 运行的服务器上部署本项目,避免端口和资源冲突。 + +## 部署架构 + +- **前端 Nginx**: 运行在 `8080` 端口(避免与现有 nginx 的 80 端口冲突) +- **后端服务**: 运行在 `3001` 端口 +- **部署路径**: `/opt/nginx/html/ai/current` +- **容器名称**: + - `ai-learning-nginx` (前端) + - `ai-learning-backend` (后端) + +## 避免冲突的设计 + +### 1. 端口隔离 +- 现有 nginx: `80:80`, `443:443` +- 本项目 nginx: `8080:80`, `8443:443` +- 后端: `3001:3001` + +### 2. 路径隔离 +- 使用独立的部署路径: `/opt/nginx/html/ai/current` +- 独立的日志目录: `/opt/nginx/html/ai/current/docker/logs` +- 独立的配置文件: `/opt/nginx/html/ai/current/docker/` + +### 3. 容器名称隔离 +- 使用唯一的容器名称,避免与现有容器冲突 + +### 4. Docker 网络隔离 +- 使用独立的 Docker 网络: `ai-learning-network` + +## 文件结构 + +部署后的目录结构: + +``` +/opt/nginx/html/ai/current/ +├── index.html # 前端入口文件 +├── assets/ # 前端静态资源 +├── backend/ # 后端文件 +│ ├── dist/ # 后端构建产物 +│ ├── prisma/ # 数据库文件 +│ ├── Dockerfile # 后端 Dockerfile +│ └── package.json +└── docker/ # Docker 配置 + ├── docker-compose.yml + ├── nginx.conf.docker + └── logs/ # Nginx 日志 +``` + +## 部署步骤 + +### 1. 通过 Jenkins 自动部署 + +Jenkins 流水线会自动: +1. 构建前端和后端 +2. 打包部署文件 +3. 传输到远程服务器 +4. 解压并部署 +5. 启动 Docker 容器 + +### 2. 手动部署 + +如果需要手动部署: + +```bash +# 1. 构建项目 +npm install +npm run install:all +npm run build + +# 2. 创建部署目录 +ssh root@180.76.180.105 "mkdir -p /opt/nginx/html/ai/current" + +# 3. 传输文件 +scp -r frontend/dist/* root@180.76.180.105:/opt/nginx/html/ai/current/ +scp -r backend/dist backend/prisma backend/package.json backend/Dockerfile root@180.76.180.105:/opt/nginx/html/ai/current/backend/ +scp nginx/docker-compose.production.yml root@180.76.180.105:/opt/nginx/html/ai/current/docker/docker-compose.yml +scp nginx/nginx.conf.docker root@180.76.180.105:/opt/nginx/html/ai/current/docker/nginx.conf.docker + +# 4. 在服务器上启动 +ssh root@180.76.180.105 << 'ENDSSH' + cd /opt/nginx/html/ai/current/docker + docker-compose up -d --build +ENDSSH +``` + +## 访问地址 + +- **前端**: http://180.76.180.105:8080 +- **后端 API**: http://180.76.180.105:3001 +- **健康检查**: http://180.76.180.105:8080/health + +## 常用命令 + +### 查看容器状态 + +```bash +cd /opt/nginx/html/ai/current/docker +docker-compose ps +``` + +### 查看日志 + +```bash +# 查看所有服务日志 +docker-compose logs -f + +# 查看 nginx 日志 +docker-compose logs -f nginx + +# 查看后端日志 +docker-compose logs -f backend +``` + +### 重启服务 + +```bash +cd /opt/nginx/html/ai/current/docker +docker-compose restart +``` + +### 停止服务 + +```bash +cd /opt/nginx/html/ai/current/docker +docker-compose down +``` + +### 重新构建并启动 + +```bash +cd /opt/nginx/html/ai/current/docker +docker-compose up -d --build +``` + +## 故障排查 + +### 1. 端口冲突 + +如果 8080 端口被占用: + +```bash +# 检查端口占用 +netstat -tulpn | grep 8080 +# 或 +lsof -i :8080 + +# 修改 docker-compose.yml 中的端口映射 +# 例如改为 8081:80 +``` + +### 2. 容器无法启动 + +```bash +# 查看容器日志 +docker-compose logs + +# 检查容器状态 +docker ps -a | grep ai-learning + +# 检查 Docker 网络 +docker network ls +``` + +### 3. 前端无法访问 + +```bash +# 检查 nginx 容器是否运行 +docker ps | grep ai-learning-nginx + +# 检查 nginx 配置 +docker exec ai-learning-nginx nginx -t + +# 查看 nginx 日志 +docker-compose logs nginx +``` + +### 4. 后端 API 无法访问 + +```bash +# 检查后端容器是否运行 +docker ps | grep ai-learning-backend + +# 检查后端日志 +docker-compose logs backend + +# 测试后端连接 +curl http://localhost:3001/api/health +``` + +### 5. 数据库问题 + +```bash +# 检查数据库文件权限 +ls -la /opt/nginx/html/ai/current/backend/prisma/ + +# 进入后端容器执行迁移 +docker exec -it ai-learning-backend sh +npx prisma migrate deploy +npx prisma generate +``` + +## 与现有 nginx 集成(可选) + +如果希望通过现有 nginx(80 端口)访问本项目,可以在现有 nginx 配置中添加反向代理: + +```nginx +# 添加到现有 nginx 配置 +location /ai/ { + proxy_pass http://localhost:8080/; + 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; +} + +location /ai/api/ { + proxy_pass http://localhost:3001/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` + +然后访问:`http://180.76.180.105/ai/` + +## 更新部署 + +每次代码更新后,Jenkins 会自动: +1. 构建新版本 +2. 备份旧版本到 `/opt/nginx/html/ai/backup-YYYYMMDD-HHMMSS` +3. 部署新版本 +4. 重启 Docker 容器 + +## 回滚 + +如果需要回滚到之前的版本: + +```bash +ssh root@180.76.180.105 << 'ENDSSH' + cd /opt/nginx/html/ai + + # 查看备份 + ls -la backup-* + + # 停止当前容器 + cd current/docker + docker-compose down + + # 恢复备份 + cd /opt/nginx/html/ai + mv current current-failed + mv backup-YYYYMMDD-HHMMSS current + + # 重启服务 + cd current/docker + docker-compose up -d +ENDSSH +``` + +## 安全建议 + +1. **不要使用 root 用户**: 建议创建专用部署用户 +2. **限制端口访问**: 使用防火墙限制 8080 端口的访问来源 +3. **定期备份**: 自动备份数据库和配置文件 +4. **监控日志**: 设置日志监控和告警 +5. **更新镜像**: 定期更新 nginx:alpine 和 node:18-alpine 镜像 diff --git a/DOCKER_ISOLATION.md b/DOCKER_ISOLATION.md new file mode 100644 index 0000000..4df4c9d --- /dev/null +++ b/DOCKER_ISOLATION.md @@ -0,0 +1,295 @@ +# Docker 隔离架构说明 + +## 隔离层次概览 + +当前项目实现了**完全隔离**的架构,从构建到运行都在 Docker 容器中,与宿主机完全隔离。 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 宿主机 (Host) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Jenkins 服务器 │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ Docker 容器: node:18 (构建阶段) │ │ │ +│ │ │ - 安装依赖 │ │ │ +│ │ │ - 构建前端 │ │ │ +│ │ │ - 构建后端 │ │ │ +│ │ │ ✅ 与 Jenkins 服务器隔离 │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ 部署到远程服务器 │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 远程服务器 (180.76.180.105) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ Docker 容器: nginx:alpine │ │ │ +│ │ │ - 端口: 8080:80 │ │ │ +│ │ │ - 网络: ai-learning-network │ │ │ +│ │ │ ✅ 与宿主机和其他容器隔离 │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ Docker 容器: backend (node:18-alpine) │ │ │ +│ │ │ - 端口: 3001:3001 │ │ │ +│ │ │ - 网络: ai-learning-network │ │ │ +│ │ │ ✅ 与宿主机和其他容器隔离 │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ 宿主机文件系统 (仅数据持久化) │ │ │ +│ │ │ - /opt/nginx/html/ai/current/ │ │ │ +│ │ │ - 数据库文件 (volume 挂载) │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 隔离层次详解 + +### 1. 构建阶段隔离(Jenkins 服务器) + +**位置**: Jenkins 服务器 +**容器**: `node:18` +**隔离内容**: + +```groovy +def nodeImage = docker.image("node:18") +nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { + // 所有构建操作都在容器中执行 + npm install + npm run build +} +``` + +**隔离效果**: +- ✅ **文件系统隔离**: 构建过程不影响 Jenkins 服务器文件系统 +- ✅ **依赖隔离**: 不需要在 Jenkins 服务器安装 Node.js、npm +- ✅ **版本隔离**: 使用固定版本的 Node.js,不受宿主机影响 +- ✅ **环境隔离**: 每次构建都是干净的环境,无历史残留 + +**好处**: +- Jenkins 服务器保持干净,不需要安装各种构建工具 +- 多个项目可以使用不同版本的 Node.js,互不干扰 +- 构建失败不会影响 Jenkins 服务器 + +### 2. 运行阶段隔离(生产服务器) + +#### 2.1 Nginx 容器隔离 + +```yaml +nginx: + image: nginx:alpine + container_name: ai-learning-nginx + ports: + - "8080:80" + networks: + - ai-learning-network +``` + +**隔离效果**: +- ✅ **进程隔离**: Nginx 进程在容器内运行,与宿主机进程隔离 +- ✅ **网络隔离**: 使用独立的 Docker 网络 `ai-learning-network` +- ✅ **文件系统隔离**: 只能访问挂载的 volume +- ✅ **端口隔离**: 使用 8080 端口,不影响宿主机 80 端口 + +#### 2.2 Backend 容器隔离 + +```yaml +backend: + build: + context: /opt/nginx/html/ai/current/backend + dockerfile: Dockerfile + container_name: ai-learning-backend + ports: + - "3001:3001" + networks: + - ai-learning-network +``` + +**隔离效果**: +- ✅ **运行时隔离**: Node.js 应用在容器内运行 +- ✅ **依赖隔离**: 不需要在宿主机安装 Node.js +- ✅ **环境变量隔离**: 容器内独立的环境变量 +- ✅ **网络隔离**: 只能通过 Docker 网络与其他容器通信 + +### 3. 网络隔离 + +```yaml +networks: + ai-learning-network: + driver: bridge +``` + +**隔离效果**: +- ✅ **独立网络**: 容器在独立的 Docker 网络中 +- ✅ **服务发现**: 容器间通过服务名通信(如 `backend:3001`) +- ✅ **端口隔离**: 容器端口不直接暴露给宿主机(除非映射) +- ✅ **安全隔离**: 外部无法直接访问容器内部网络 + +## 完全隔离的好处 + +### 1. 安全性 + +``` +宿主机 ← 完全隔离 → 应用容器 +``` + +- ✅ **进程隔离**: 容器崩溃不影响宿主机 +- ✅ **文件系统隔离**: 容器无法访问宿主机敏感文件 +- ✅ **网络隔离**: 容器间通信受控 +- ✅ **权限隔离**: 容器以非 root 用户运行(可配置) + +### 2. 环境一致性 + +``` +开发环境 = 构建环境 = 生产环境 +``` + +- ✅ **版本一致**: 所有环境使用相同的 Node.js 版本 +- ✅ **依赖一致**: 依赖版本锁定在 package.json +- ✅ **配置一致**: Docker 配置统一管理 + +### 3. 资源隔离 + +``` +容器 A ← 资源限制 → 容器 B +``` + +- ✅ **CPU 限制**: 可以限制容器 CPU 使用率 +- ✅ **内存限制**: 可以限制容器内存使用 +- ✅ **磁盘限制**: 可以限制容器磁盘使用 + +### 4. 易于管理 + +- ✅ **独立更新**: 可以单独更新某个容器,不影响其他 +- ✅ **快速回滚**: 可以快速回滚到之前的镜像版本 +- ✅ **日志隔离**: 每个容器的日志独立管理 +- ✅ **监控隔离**: 可以单独监控每个容器 + +## 数据持久化(唯一与宿主机共享的部分) + +虽然容器是隔离的,但某些数据需要持久化: + +```yaml +volumes: + # 数据库文件(需要持久化) + - /opt/nginx/html/ai/current/backend/prisma:/app/prisma + + # 前端静态文件(只读) + - /opt/nginx/html/ai/current:/usr/share/nginx/html:ro + + # 日志文件(需要持久化) + - /opt/nginx/html/ai/current/docker/logs:/var/log/nginx +``` + +**说明**: +- 数据库文件需要持久化,所以挂载到宿主机 +- 前端文件是只读挂载,容器无法修改 +- 日志文件挂载到宿主机,方便查看和管理 + +## 隔离验证 + +### 1. 检查容器隔离 + +```bash +# 查看运行中的容器 +docker ps + +# 查看容器网络 +docker network inspect ai-learning-network + +# 查看容器进程(在宿主机上) +ps aux | grep nginx # 看不到容器内的进程 + +# 进入容器查看 +docker exec -it ai-learning-nginx sh +# 在容器内:无法访问宿主机其他文件(除了挂载的 volume) +``` + +### 2. 检查网络隔离 + +```bash +# 在宿主机上无法直接访问容器内部网络 +curl http://backend:3001 # ❌ 失败,backend 只在 Docker 网络内可访问 + +# 在容器内可以访问 +docker exec -it ai-learning-nginx sh +curl http://backend:3001 # ✅ 成功,在同一 Docker 网络内 +``` + +### 3. 检查文件系统隔离 + +```bash +# 容器内的文件系统是独立的 +docker exec -it ai-learning-backend ls /app +# 只能看到容器内的文件,看不到宿主机其他目录(除了挂载的 volume) +``` + +## 安全建议 + +虽然已经实现了完全隔离,但还可以进一步加强: + +### 1. 使用非 root 用户运行容器 + +```dockerfile +# 在 Dockerfile 中 +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 +USER nodejs +``` + +### 2. 限制资源使用 + +```yaml +services: + backend: + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M +``` + +### 3. 只读文件系统(除了必要目录) + +```yaml +services: + nginx: + read_only: true + tmpfs: + - /tmp + - /var/cache/nginx +``` + +### 4. 网络策略 + +```yaml +services: + backend: + networks: + - ai-learning-network + # 不暴露端口到宿主机,只允许 nginx 访问 + # ports: [] # 注释掉端口映射 +``` + +## 总结 + +**是的,当前架构实现了完全隔离**: + +1. ✅ **构建阶段**: 在 Docker 容器中构建,与 Jenkins 服务器隔离 +2. ✅ **运行阶段**: 应用在 Docker 容器中运行,与宿主机隔离 +3. ✅ **网络隔离**: 容器在独立的 Docker 网络中 +4. ✅ **文件系统隔离**: 容器只能访问挂载的 volume +5. ✅ **进程隔离**: 容器进程与宿主机进程隔离 + +**唯一与宿主机共享的**: +- 数据文件(数据库、日志)- 通过 volume 挂载 +- 端口映射(8080, 3001)- 用于外部访问 + +这种架构提供了**最大的安全性和可移植性**,同时保持了必要的数据持久化。 diff --git a/JENKINS_DEPLOY.md b/JENKINS_DEPLOY.md new file mode 100644 index 0000000..de09145 --- /dev/null +++ b/JENKINS_DEPLOY.md @@ -0,0 +1,264 @@ +# Jenkins 部署配置说明 + +本文档说明如何通过 Jenkins 将项目部署到远程服务器 `180.76.180.105` 的 `/opt/nginx/html/ai` 目录。 + +## 前置要求 + +### 1. Jenkins 服务器要求 +- Jenkins 已安装并运行 +- 安装了必要的插件: + - Pipeline 插件 + - SSH Pipeline Steps 插件(可选,用于更好的 SSH 支持) +- Node.js 和 npm 已安装(用于构建) + +### 2. 远程服务器要求 +- 服务器 IP: `180.76.180.105` +- 部署目录: `/opt/nginx/html/ai` +- 需要安装: + - Node.js (推荐 v18+) + - npm + - nginx + - PM2(用于管理后端进程,可选) + +### 3. SSH 配置 +确保 Jenkins 服务器可以通过 SSH 无密码登录到远程服务器: + +```bash +# 在 Jenkins 服务器上生成 SSH 密钥(如果还没有) +ssh-keygen -t rsa -b 4096 + +# 将公钥复制到远程服务器 +ssh-copy-id root@180.76.180.105 + +# 测试连接 +ssh root@180.76.180.105 +``` + +或者在 Jenkins 中配置 SSH 凭据: +1. 进入 Jenkins → 系统管理 → 凭据管理 +2. 添加 SSH 凭据 +3. 在 Jenkinsfile 中使用 `SSH_CREDENTIALS` 环境变量 + +## Jenkins 配置步骤 + +### 1. 创建 Pipeline 任务 + +1. 在 Jenkins 中创建新任务 +2. 选择 "流水线" (Pipeline) 类型 +3. 在 "流水线" 配置中: + - **定义**: Pipeline script from SCM + - **SCM**: Git + - **Repository URL**: 你的 Git 仓库地址 + - **脚本路径**: Jenkinsfile + +### 2. 配置环境变量(可选) + +如果需要修改默认配置,可以在 Jenkinsfile 中修改 `environment` 部分: + +```groovy +environment { + DEPLOY_HOST = '180.76.180.105' + DEPLOY_USER = 'root' // 根据实际情况修改 + DEPLOY_PATH = '/opt/nginx/html/ai' +} +``` + +### 3. 配置 SSH 凭据(推荐) + +如果使用 SSH 凭据而不是密钥文件: + +1. 在 Jenkins 中添加 SSH 凭据(ID: `deploy-ssh-key`) +2. 修改 Jenkinsfile 中的部署步骤,使用 `withCredentials`: + +```groovy +withCredentials([sshUserPrivateKey(credentialsId: 'deploy-ssh-key', keyFileVariable: 'SSH_KEY')]) { + sh ''' + ssh -i $SSH_KEY -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} ... + ''' +} +``` + +## 部署流程 + +Jenkins 流水线将执行以下步骤: + +1. **Checkout**: 检出代码 +2. **Install Dependencies**: 安装所有依赖 +3. **Build Frontend**: 构建前端(生成 `frontend/dist`) +4. **Build Backend**: 构建后端(生成 `backend/dist`) +5. **Prepare Deployment Package**: 打包部署文件 +6. **Deploy to Remote Server**: + - 传输文件到远程服务器 + - 解压并部署到 `/opt/nginx/html/ai/current` + - 备份旧版本 +7. **Restart Services**: 重启后端服务和 nginx + +## 远程服务器配置 + +### 1. 创建部署目录 + +```bash +sudo mkdir -p /opt/nginx/html/ai +sudo chown -R $USER:$USER /opt/nginx/html/ai +``` + +### 2. 配置 Nginx + +将 `nginx/nginx.conf` 复制到系统 nginx 配置目录: + +```bash +# Ubuntu/Debian +sudo cp /opt/nginx/html/ai/current/nginx/nginx.conf /etc/nginx/sites-available/ai-learning-platform +sudo ln -s /etc/nginx/sites-available/ai-learning-platform /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +``` + +**注意**: 需要修改 nginx.conf 中的前端文件路径: + +```nginx +location / { + root /opt/nginx/html/ai/current; # 修改这里 + index index.html; + try_files $uri $uri/ /index.html; +} +``` + +### 3. 配置后端服务 + +#### 使用 PM2(推荐) + +```bash +# 安装 PM2 +npm install -g pm2 + +# 启动后端服务 +cd /opt/nginx/html/ai/current/backend +pm2 start dist/index.js --name ai-learning-backend +pm2 save +pm2 startup # 设置开机自启 +``` + +#### 使用 systemd + +创建 systemd 服务文件 `/etc/systemd/system/ai-learning-backend.service`: + +```ini +[Unit] +Description=AI Learning Platform Backend +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/nginx/html/ai/current/backend +ExecStart=/usr/bin/node dist/index.js +Restart=always +RestartSec=10 +Environment=NODE_ENV=production +Environment=PORT=3001 + +[Install] +WantedBy=multi-user.target +``` + +然后启用服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable ai-learning-backend +sudo systemctl start ai-learning-backend +``` + +## 手动部署(测试) + +如果需要手动测试部署流程: + +```bash +# 1. 构建项目 +npm install +npm run install:all +npm run build + +# 2. 创建部署包 +mkdir -p deploy-package +cp -r frontend/dist deploy-package/frontend-dist +cp -r backend/dist deploy-package/backend/dist +cp -r backend/prisma deploy-package/backend/prisma +cp backend/package.json deploy-package/backend/ +cp nginx/nginx.conf deploy-package/nginx/ +cp scripts/deploy.sh deploy-package/ +tar -czf deploy-package.tar.gz deploy-package/ + +# 3. 传输到远程服务器 +scp deploy-package.tar.gz root@180.76.180.105:/tmp/ + +# 4. 在远程服务器上执行 +ssh root@180.76.180.105 +cd /tmp +tar -xzf deploy-package.tar.gz +mkdir -p /opt/nginx/html/ai/current +cp -r deploy-package/frontend-dist/* /opt/nginx/html/ai/current/ +cp -r deploy-package/backend /opt/nginx/html/ai/current/ +bash deploy-package/deploy.sh /opt/nginx/html/ai/current +``` + +## 故障排查 + +### 1. SSH 连接失败 + +- 检查网络连接 +- 确认 SSH 密钥配置正确 +- 检查防火墙设置 + +### 2. 构建失败 + +- 检查 Node.js 版本 +- 确认所有依赖已安装 +- 查看 Jenkins 构建日志 + +### 3. 部署后服务无法访问 + +- 检查 nginx 配置是否正确 +- 确认后端服务是否运行:`pm2 list` 或 `systemctl status ai-learning-backend` +- 检查端口是否被占用:`netstat -tulpn | grep 3001` +- 查看日志:`pm2 logs ai-learning-backend` 或 `journalctl -u ai-learning-backend` + +### 4. 数据库问题 + +- 确认数据库文件权限 +- 检查 Prisma 迁移是否成功 +- 查看数据库文件:`ls -la /opt/nginx/html/ai/current/backend/prisma/` + +## 安全建议 + +1. **不要使用 root 用户**: 建议创建专用部署用户 +2. **使用 SSH 密钥**: 避免使用密码认证 +3. **限制 SSH 访问**: 使用防火墙限制 SSH 访问来源 +4. **定期备份**: 自动备份数据库和配置文件 +5. **监控日志**: 设置日志监控和告警 + +## 更新部署 + +每次代码更新后,只需在 Jenkins 中触发构建即可。Jenkins 会自动: +- 构建最新代码 +- 部署到远程服务器 +- 备份旧版本 +- 重启服务 + +## 回滚 + +如果需要回滚到之前的版本: + +```bash +ssh root@180.76.180.105 +cd /opt/nginx/html/ai +# 查看备份目录 +ls -la backup-* +# 恢复备份 +mv current current-failed +mv backup-YYYYMMDD-HHMMSS current +# 重启服务 +pm2 restart ai-learning-backend +nginx -s reload +``` diff --git a/JENKINS_DOCKER.md b/JENKINS_DOCKER.md new file mode 100644 index 0000000..6e3a370 --- /dev/null +++ b/JENKINS_DOCKER.md @@ -0,0 +1,334 @@ +# Jenkins Docker 构建配置说明 + +本文档说明如何在 Jenkins 中使用 Docker 容器执行构建和部署。 + +## Jenkins Docker 支持 + +Jenkins 支持在 Docker 容器中执行构建步骤,这提供了以下优势: + +1. **环境隔离**: 每次构建都在干净的容器环境中执行 +2. **版本一致性**: 使用固定版本的 Node.js,避免环境差异 +3. **易于维护**: 不需要在 Jenkins 服务器上安装 Node.js +4. **可移植性**: 构建环境与代码一起定义 + +## 前置要求 + +### 1. 安装 Jenkins Docker 插件 + +在 Jenkins 中安装以下插件: +- **Docker Pipeline** (推荐) +- **Docker** (可选,用于 Docker 命令支持) + +安装步骤: +1. 进入 Jenkins → 系统管理 → 插件管理 +2. 搜索 "Docker Pipeline" +3. 安装并重启 Jenkins + +### 2. 配置 Docker + +确保 Jenkins 服务器可以访问 Docker: + +```bash +# 检查 Docker 是否运行 +docker ps + +# 确保 Jenkins 用户有权限访问 Docker socket +# 方法1: 将 Jenkins 用户添加到 docker 组 +sudo usermod -aG docker jenkins + +# 方法2: 修改 Docker socket 权限(不推荐,安全风险) +sudo chmod 666 /var/run/docker.sock +``` + +### 3. 测试 Docker 访问 + +在 Jenkins 中创建一个测试 Pipeline: + +```groovy +pipeline { + agent any + stages { + stage('Test Docker') { + steps { + script { + def nodeImage = docker.image("node:18") + nodeImage.inside() { + sh 'node --version' + } + } + } + } + } +} +``` + +## 两种 Jenkinsfile 配置 + +### 方式一:完全 Docker 化(推荐) + +使用 `Jenkinsfile.docker`,所有构建步骤都在 Docker 容器中执行: + +```groovy +stage('Build in Docker') { + steps { + script { + def nodeImage = docker.image("node:18") + nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { + sh ''' + npm install + npm run build + ''' + } + } + } +} +``` + +**优点**: +- 完全隔离的构建环境 +- 不依赖 Jenkins 服务器的 Node.js 版本 +- 每次构建都是干净的环境 + +**缺点**: +- 需要 Docker Pipeline 插件 +- 首次构建需要下载 Docker 镜像 + +### 方式二:混合模式(当前 Jenkinsfile) + +部分步骤在 Docker 中,部分在主机上: + +```groovy +stage('Build in Docker') { + steps { + script { + def nodeImage = docker.image("node:18") + nodeImage.inside() { + sh 'npm run build' + } + } + } +} +``` + +## 使用 Docker 构建的 Jenkinsfile + +项目提供了两个版本的 Jenkinsfile: + +1. **Jenkinsfile** - 当前版本,支持 Docker 构建 +2. **Jenkinsfile.docker** - 完全 Docker 化版本 + +### 切换到 Docker 版本 + +如果需要使用完全 Docker 化的版本: + +```bash +# 备份当前版本 +cp Jenkinsfile Jenkinsfile.original + +# 使用 Docker 版本 +cp Jenkinsfile.docker Jenkinsfile +``` + +或者在 Jenkins 中直接指定文件路径: +- Pipeline script from SCM +- Script Path: `Jenkinsfile.docker` + +## Docker 构建配置说明 + +### 1. Node.js 版本 + +在 Jenkinsfile 中指定 Node.js 版本: + +```groovy +environment { + NODE_VERSION = '18' // 或 '20', '18-alpine' 等 +} +``` + +### 2. Docker Socket 挂载 + +如果需要构建 Docker 镜像,需要挂载 Docker socket: + +```groovy +nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { + // 可以在这里执行 docker build 等命令 +} +``` + +### 3. Volume 挂载 + +如果需要持久化构建产物,可以挂载卷: + +```groovy +nodeImage.inside('-v /workspace:/workspace') { + // 构建产物会保留在 /workspace +} +``` + +## 常见问题 + +### 1. Docker 命令未找到 + +**错误**: `docker: command not found` + +**解决**: +- 确保安装了 Docker Pipeline 插件 +- 检查 Jenkins 用户是否有 Docker 访问权限 + +### 2. 权限 denied + +**错误**: `permission denied while trying to connect to the Docker daemon socket` + +**解决**: +```bash +sudo usermod -aG docker jenkins +sudo systemctl restart jenkins +``` + +### 3. 镜像拉取失败 + +**错误**: `Error pulling image` + +**解决**: +- 检查网络连接 +- 配置 Docker 镜像加速器 +- 使用国内镜像源(如阿里云) + +### 4. 构建产物丢失 + +**问题**: Docker 容器退出后,构建产物丢失 + +**解决**: +- Jenkins 会自动将工作目录挂载到容器中 +- 确保构建产物在 `$WORKSPACE` 目录下 +- 使用 `docker.inside()` 会自动处理工作目录 + +## 性能优化 + +### 1. 使用 Docker 镜像缓存 + +Jenkins 会自动缓存 Docker 镜像,但可以手动拉取: + +```groovy +stage('Pull Docker Image') { + steps { + script { + docker.image('node:18').pull() + } + } +} +``` + +### 2. 使用 Alpine 镜像 + +使用更小的 Alpine 镜像可以加快拉取速度: + +```groovy +def nodeImage = docker.image("node:18-alpine") +``` + +### 3. 并行构建 + +可以在不同的 Docker 容器中并行构建前端和后端: + +```groovy +stage('Parallel Build') { + parallel { + stage('Build Frontend') { + steps { + script { + docker.image('node:18').inside() { + sh 'npm run build --workspace=frontend' + } + } + } + } + stage('Build Backend') { + steps { + script { + docker.image('node:18').inside() { + sh 'cd backend && npm run build' + } + } + } + } + } +} +``` + +## 最佳实践 + +1. **固定版本**: 使用具体的 Node.js 版本标签(如 `node:18`),避免使用 `latest` +2. **缓存依赖**: 使用 npm cache 或 Docker layer caching +3. **清理资源**: 在 `post` 阶段清理 Docker 资源 +4. **错误处理**: 添加错误处理和日志记录 +5. **安全扫描**: 定期扫描 Docker 镜像的安全漏洞 + +## 示例:完整的 Docker 构建 Pipeline + +```groovy +pipeline { + agent any + + environment { + NODE_VERSION = '18' + DOCKER_REGISTRY = 'your-registry.com' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Build') { + steps { + script { + def nodeImage = docker.image("node:${NODE_VERSION}") + nodeImage.inside() { + sh ''' + npm ci + npm run build + ''' + } + } + } + } + + stage('Test') { + steps { + script { + docker.image("node:${NODE_VERSION}").inside() { + sh 'npm test' + } + } + } + } + + stage('Build Docker Images') { + steps { + script { + docker.build("myapp:${env.BUILD_NUMBER}") + } + } + } + } + + post { + always { + sh 'docker system prune -f' + } + } +} +``` + +## 总结 + +使用 Docker 构建的优势: +- ✅ 环境一致性 +- ✅ 易于维护 +- ✅ 可移植性 +- ✅ 隔离性 + +当前项目已支持 Docker 构建,只需确保 Jenkins 安装了 Docker Pipeline 插件即可使用。 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..8409611 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,196 @@ +pipeline { + agent any + + environment { + // 远程服务器配置 + DEPLOY_HOST = '180.76.180.105' + DEPLOY_USER = 'root' // 根据实际情况修改用户名 + DEPLOY_PATH = '/opt/nginx/html/ai' + // 如果需要 SSH 密钥,可以在 Jenkins 中配置 SSH credentials + // SSH_CREDENTIALS = credentials('deploy-ssh-key') + } + + stages { + stage('Checkout') { + steps { + echo '检出代码...' + checkout scm + } + } + + stage('Build in Docker') { + steps { + script { + // 在 Node.js Docker 容器中执行构建 + // 需要 Jenkins 安装 Docker Pipeline 插件 + def nodeImage = docker.image("node:18") + nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { + sh ''' + echo "在 Docker 容器中构建..." + echo "Node 版本:" + node --version + echo "NPM 版本:" + npm --version + + # 安装依赖 + echo "安装依赖..." + npm install + npm run install:all + + # 构建前端 + echo "构建前端..." + npm run build --workspace=frontend + + # 构建后端 + echo "构建后端..." + cd backend + npm run build + npm run prisma:generate + cd .. + ''' + } + } + } + } + + stage('Prepare Deployment Package') { + steps { + echo '准备部署包...' + sh ''' + # 创建临时部署目录 + mkdir -p deploy-package + + # 复制前端构建产物 + cp -r frontend/dist deploy-package/frontend-dist + + # 复制后端构建产物和必要文件 + mkdir -p deploy-package/backend + cp -r backend/dist deploy-package/backend/dist + cp -r backend/prisma deploy-package/backend/prisma + cp backend/package.json deploy-package/backend/ + cp backend/package-lock.json deploy-package/backend/ 2>/dev/null || true + + # 复制 Docker 相关文件 + mkdir -p deploy-package/docker + cp nginx/docker-compose.production.yml deploy-package/docker/docker-compose.yml + cp nginx/nginx.conf.docker deploy-package/docker/nginx.conf.docker + if [ -f backend/Dockerfile ]; then + cp backend/Dockerfile deploy-package/backend/ + fi + + # 复制部署脚本 + cp scripts/deploy-docker.sh deploy-package/ + chmod +x deploy-package/deploy-docker.sh + + # 创建部署包 + tar -czf deploy-package.tar.gz deploy-package/ + ''' + } + } + + stage('Deploy to Remote Server') { + steps { + echo '部署到远程服务器...' + script { + // 使用 SSH 传输文件并执行部署 + sh ''' + # 传输部署包到远程服务器 + scp -o StrictHostKeyChecking=no deploy-package.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ + + # 在远程服务器上执行部署 + ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' + # 创建部署目录 + mkdir -p /opt/nginx/html/ai + + # 解压部署包 + cd /tmp + tar -xzf deploy-package.tar.gz + + # 备份旧版本(如果存在) + if [ -d /opt/nginx/html/ai/current ]; then + mv /opt/nginx/html/ai/current /opt/nginx/html/ai/backup-$(date +%Y%m%d-%H%M%S) + fi + + # 创建新版本目录 + mkdir -p /opt/nginx/html/ai/current + + # 复制前端文件 + cp -r deploy-package/frontend-dist/* /opt/nginx/html/ai/current/ + + # 复制后端文件 + mkdir -p /opt/nginx/html/ai/current/backend + cp -r deploy-package/backend/* /opt/nginx/html/ai/current/backend/ + + # 复制 Docker 配置文件 + mkdir -p /opt/nginx/html/ai/current/docker + cp deploy-package/docker/docker-compose.yml /opt/nginx/html/ai/current/docker/ + cp deploy-package/docker/nginx.conf.docker /opt/nginx/html/ai/current/docker/ + + # 创建 nginx logs 目录 + mkdir -p /opt/nginx/html/ai/current/docker/logs + + # 执行 Docker 部署脚本 + if [ -f deploy-package/deploy-docker.sh ]; then + chmod +x deploy-package/deploy-docker.sh + bash deploy-package/deploy-docker.sh /opt/nginx/html/ai/current + fi + + # 清理临时文件 + rm -rf /tmp/deploy-package /tmp/deploy-package.tar.gz + + echo "部署完成!" + ENDSSH + ''' + } + } + } + + stage('Restart Services') { + steps { + echo '重启 Docker 服务...' + sh ''' + ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' + # 使用 Docker Compose 重启服务 + cd /opt/nginx/html/ai/current/docker + + # 检查 docker-compose 命令(支持新版本的 docker compose) + if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + COMPOSE_CMD="docker compose" + fi + + # 停止旧容器 + $COMPOSE_CMD down || true + + # 重新构建并启动 + $COMPOSE_CMD up -d --build + + # 等待服务启动 + sleep 5 + + # 检查服务状态 + $COMPOSE_CMD ps + + echo "Docker 服务已重启" + echo "前端访问: http://180.76.180.105:8080" + echo "后端 API: http://180.76.180.105:3001" + ENDSSH + ''' + } + } + } + + post { + success { + echo '部署成功!' + } + failure { + echo '部署失败!' + } + always { + // 清理本地临时文件 + sh 'rm -rf deploy-package deploy-package.tar.gz' + } + } +} diff --git a/Jenkinsfile.docker b/Jenkinsfile.docker new file mode 100644 index 0000000..5cb5560 --- /dev/null +++ b/Jenkinsfile.docker @@ -0,0 +1,195 @@ +pipeline { + agent any + + environment { + // 远程服务器配置 + DEPLOY_HOST = '180.76.180.105' + DEPLOY_USER = 'root' + DEPLOY_PATH = '/opt/nginx/html/ai' + // Node.js 版本 + NODE_VERSION = '18' + } + + stages { + stage('Checkout') { + steps { + echo '检出代码...' + checkout scm + } + } + + stage('Build in Docker') { + steps { + script { + // 在 Node.js Docker 容器中执行构建 + def nodeImage = docker.image("node:${NODE_VERSION}") + nodeImage.inside('-v /var/run/docker.sock:/var/run/docker.sock') { + sh ''' + echo "在 Docker 容器中构建..." + echo "Node 版本:" + node --version + echo "NPM 版本:" + npm --version + + # 安装依赖 + echo "安装依赖..." + npm install + npm run install:all + + # 构建前端 + echo "构建前端..." + npm run build --workspace=frontend + + # 构建后端 + echo "构建后端..." + cd backend + npm run build + npm run prisma:generate + cd .. + ''' + } + } + } + } + + stage('Prepare Deployment Package') { + steps { + script { + // 在 Docker 容器中打包 + def nodeImage = docker.image("node:${NODE_VERSION}") + nodeImage.inside() { + sh ''' + echo "准备部署包..." + + # 创建临时部署目录 + mkdir -p deploy-package + + # 复制前端构建产物 + cp -r frontend/dist deploy-package/frontend-dist + + # 复制后端构建产物和必要文件 + mkdir -p deploy-package/backend + cp -r backend/dist deploy-package/backend/dist + cp -r backend/prisma deploy-package/backend/prisma + cp backend/package.json deploy-package/backend/ + cp backend/package-lock.json deploy-package/backend/ 2>/dev/null || true + + # 复制 Docker 相关文件 + mkdir -p deploy-package/docker + cp nginx/docker-compose.production.yml deploy-package/docker/docker-compose.yml + cp nginx/nginx.conf.docker deploy-package/docker/nginx.conf.docker + if [ -f backend/Dockerfile ]; then + cp backend/Dockerfile deploy-package/backend/ + fi + + # 复制部署脚本 + cp scripts/deploy-docker.sh deploy-package/ + chmod +x deploy-package/deploy-docker.sh + + # 创建部署包 + tar -czf deploy-package.tar.gz deploy-package/ + ''' + } + } + } + } + + stage('Deploy to Remote Server') { + steps { + echo '部署到远程服务器...' + script { + sh ''' + # 传输部署包到远程服务器 + scp -o StrictHostKeyChecking=no deploy-package.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ + + # 在远程服务器上执行部署 + ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' + # 创建部署目录 + mkdir -p /opt/nginx/html/ai + + # 解压部署包 + cd /tmp + tar -xzf deploy-package.tar.gz + + # 备份旧版本(如果存在) + if [ -d /opt/nginx/html/ai/current ]; then + mv /opt/nginx/html/ai/current /opt/nginx/html/ai/backup-$(date +%Y%m%d-%H%M%S) + fi + + # 创建新版本目录 + mkdir -p /opt/nginx/html/ai/current + + # 复制前端文件 + cp -r deploy-package/frontend-dist/* /opt/nginx/html/ai/current/ + + # 复制后端文件 + mkdir -p /opt/nginx/html/ai/current/backend + cp -r deploy-package/backend/* /opt/nginx/html/ai/current/backend/ + + # 复制 Docker 配置文件 + mkdir -p /opt/nginx/html/ai/current/docker + cp deploy-package/docker/docker-compose.yml /opt/nginx/html/ai/current/docker/ + cp deploy-package/docker/nginx.conf.docker /opt/nginx/html/ai/current/docker/ + + # 创建 nginx logs 目录 + mkdir -p /opt/nginx/html/ai/current/docker/logs + + # 清理临时文件 + rm -rf /tmp/deploy-package /tmp/deploy-package.tar.gz + + echo "部署完成!" + ENDSSH + ''' + } + } + } + + stage('Restart Docker Services') { + steps { + echo '重启 Docker 服务...' + sh ''' + ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH' + # 使用 Docker Compose 重启服务 + cd /opt/nginx/html/ai/current/docker + + # 检查 docker-compose 命令(支持新版本的 docker compose) + if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + COMPOSE_CMD="docker compose" + fi + + # 停止旧容器 + $COMPOSE_CMD down || true + + # 重新构建并启动 + $COMPOSE_CMD up -d --build + + # 等待服务启动 + sleep 5 + + # 检查服务状态 + $COMPOSE_CMD ps + + echo "Docker 服务已重启" + echo "前端访问: http://180.76.180.105:8080" + echo "后端 API: http://180.76.180.105:3001" + ENDSSH + ''' + } + } + } + + post { + success { + echo '部署成功!' + } + failure { + echo '部署失败!' + } + always { + // 清理本地临时文件 + sh 'rm -rf deploy-package deploy-package.tar.gz' + } + } +} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..bda2b80 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine + +WORKDIR /app + +# 复制 package 文件 +COPY package*.json ./ +COPY prisma ./prisma/ + +# 安装依赖(包括生产依赖) +RUN npm ci --production + +# 复制构建产物 +COPY dist ./dist + +# 生成 Prisma Client +RUN npx prisma generate + +# 暴露端口 +EXPOSE 3001 + +# 启动应用 +CMD ["node", "dist/index.js"] diff --git a/nginx/docker-compose.production.yml b/nginx/docker-compose.production.yml new file mode 100644 index 0000000..686937f --- /dev/null +++ b/nginx/docker-compose.production.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + # 前端 Nginx 服务(使用 8080 端口避免与现有 nginx 冲突) + nginx: + image: nginx:alpine + container_name: ai-learning-nginx + ports: + - "8080:80" # 使用 8080 端口,避免与现有 nginx 冲突 + - "8443:443" # HTTPS 端口(可选) + volumes: + - /opt/nginx/html/ai/current/docker/nginx.conf.docker:/etc/nginx/conf.d/default.conf:ro + - /opt/nginx/html/ai/current:/usr/share/nginx/html:ro + - /opt/nginx/html/ai/current/docker/logs:/var/log/nginx + depends_on: + - backend + restart: unless-stopped + networks: + - ai-learning-network + + # 后端服务 + backend: + build: + context: /opt/nginx/html/ai/current/backend + dockerfile: Dockerfile + container_name: ai-learning-backend + ports: + - "3001:3001" + environment: + - NODE_ENV=production + - PORT=3001 + - DATABASE_URL=file:./prisma/dev.db + volumes: + - /opt/nginx/html/ai/current/backend/prisma:/app/prisma + - /opt/nginx/html/ai/current/backend/dist:/app/dist:ro + restart: unless-stopped + networks: + - ai-learning-network + +networks: + ai-learning-network: + driver: bridge diff --git a/nginx/nginx.conf b/nginx/nginx.conf index b5695b6..519f2ae 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -20,7 +20,7 @@ server { # 前端静态文件 location / { - root /var/www/ai-learning-platform/frontend/dist; + root /opt/nginx/html/ai/current; index index.html; try_files $uri $uri/ /index.html; diff --git a/nginx/nginx.conf.docker b/nginx/nginx.conf.docker new file mode 100644 index 0000000..29e89a2 --- /dev/null +++ b/nginx/nginx.conf.docker @@ -0,0 +1,59 @@ +# Nginx 配置文件 - Docker 部署版本 +# 用于 docker-compose 部署,使用 Docker 服务名进行服务发现 + +# 上游后端服务器(使用 Docker 服务名) +upstream backend { + server backend:3001; # 使用 Docker Compose 服务名 + keepalive 64; +} + +# HTTP 服务器配置 +server { + listen 80; + server_name _; # 匹配所有域名和 IP + + # 客户端最大请求体大小 + client_max_body_size 10M; + + # 日志配置 + access_log /var/log/nginx/ai-learning-access.log; + error_log /var/log/nginx/ai-learning-error.log; + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # API 代理 + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # 健康检查 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} diff --git a/nginx/nginx.conf.production b/nginx/nginx.conf.production new file mode 100644 index 0000000..e50dbb4 --- /dev/null +++ b/nginx/nginx.conf.production @@ -0,0 +1,128 @@ +# Nginx 配置文件 - AI学习平台(生产环境) +# 部署路径: /opt/nginx/html/ai/current + +# 上游后端服务器 +upstream backend { + server localhost:3001; + keepalive 64; +} + +# HTTP 服务器配置 +server { + listen 80; + server_name 180.76.180.105; # 服务器 IP 或域名 + + # 客户端最大请求体大小 + client_max_body_size 10M; + + # 日志配置 + access_log /var/log/nginx/ai-learning-access.log; + error_log /var/log/nginx/ai-learning-error.log; + + # 前端静态文件 + location / { + root /opt/nginx/html/ai/current; + index index.html; + try_files $uri $uri/ /index.html; + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # API 代理 + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # 健康检查 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} + +# HTTPS 服务器配置(可选,需要 SSL 证书) +# server { +# listen 443 ssl http2; +# server_name 180.76.180.105; # 服务器 IP 或域名 +# +# # SSL 证书配置 +# ssl_certificate /path/to/your/certificate.crt; +# ssl_certificate_key /path/to/your/private.key; +# +# # SSL 优化配置 +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers HIGH:!aNULL:!MD5; +# ssl_prefer_server_ciphers on; +# ssl_session_cache shared:SSL:10m; +# ssl_session_timeout 10m; +# +# # 客户端最大请求体大小 +# client_max_body_size 10M; +# +# # 日志配置 +# access_log /var/log/nginx/ai-learning-ssl-access.log; +# error_log /var/log/nginx/ai-learning-ssl-error.log; +# +# # 前端静态文件 +# location / { +# root /opt/nginx/html/ai/current; +# index index.html; +# try_files $uri $uri/ /index.html; +# +# # 静态资源缓存 +# location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { +# expires 1y; +# add_header Cache-Control "public, immutable"; +# } +# } +# +# # API 代理 +# location /api { +# proxy_pass http://backend; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# 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; +# proxy_cache_bypass $http_upgrade; +# +# # 超时设置 +# proxy_connect_timeout 60s; +# proxy_send_timeout 60s; +# proxy_read_timeout 60s; +# } +# +# # 健康检查 +# location /health { +# access_log off; +# return 200 "healthy\n"; +# add_header Content-Type text/plain; +# } +# } +# +# # HTTP 重定向到 HTTPS +# server { +# listen 80; +# server_name 180.76.180.105; # 服务器 IP 或域名 +# return 301 https://$server_name$request_uri; +# } diff --git a/scripts/deploy-docker.sh b/scripts/deploy-docker.sh new file mode 100755 index 0000000..8ca012e --- /dev/null +++ b/scripts/deploy-docker.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Docker 部署脚本 - 在远程服务器上执行 +# 用法: ./deploy-docker.sh <部署目录> + +set -e + +DEPLOY_DIR=${1:-/opt/nginx/html/ai/current} +DOCKER_DIR="$DEPLOY_DIR/docker" + +echo "开始 Docker 部署到: $DEPLOY_DIR" + +# 检查部署目录是否存在 +if [ ! -d "$DEPLOY_DIR" ]; then + echo "错误: 部署目录不存在: $DEPLOY_DIR" + exit 1 +fi + +# 检查 Docker 是否安装 +if ! command -v docker &> /dev/null; then + echo "错误: Docker 未安装" + exit 1 +fi + +if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then + echo "错误: docker-compose 未安装" + exit 1 +fi + +# 检查 docker-compose 命令(支持新版本的 docker compose) +COMPOSE_CMD="docker-compose" +if ! command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker compose" +fi + +# 进入 Docker 目录 +if [ ! -d "$DOCKER_DIR" ]; then + echo "错误: Docker 配置目录不存在: $DOCKER_DIR" + exit 1 +fi + +cd "$DOCKER_DIR" || exit 1 + +# 停止旧容器(如果存在) +echo "停止旧容器..." +$COMPOSE_CMD down || true + +# 构建并启动服务 +echo "构建并启动 Docker 服务..." +$COMPOSE_CMD up -d --build + +# 等待服务启动 +echo "等待服务启动..." +sleep 5 + +# 检查服务状态 +echo "检查服务状态..." +$COMPOSE_CMD ps + +# 检查容器健康状态 +echo "检查容器健康状态..." +if docker ps | grep -q ai-learning-nginx; then + echo "✅ Nginx 容器运行中" +else + echo "❌ Nginx 容器未运行" + exit 1 +fi + +if docker ps | grep -q ai-learning-backend; then + echo "✅ Backend 容器运行中" +else + echo "❌ Backend 容器未运行" + exit 1 +fi + +echo "" +echo "==========================================" +echo "Docker 部署完成!" +echo "==========================================" +echo "前端访问: http://180.76.180.105:8080" +echo "后端 API: http://180.76.180.105:3001" +echo "健康检查: http://180.76.180.105:8080/health" +echo "==========================================" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..40cd1d3 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# 部署脚本 - 在远程服务器上执行 +# 用法: ./deploy.sh <部署目录> + +set -e + +DEPLOY_DIR=${1:-/opt/nginx/html/ai/current} +BACKEND_DIR="$DEPLOY_DIR/backend" + +echo "开始部署到: $DEPLOY_DIR" + +# 检查部署目录是否存在 +if [ ! -d "$DEPLOY_DIR" ]; then + echo "错误: 部署目录不存在: $DEPLOY_DIR" + exit 1 +fi + +# 进入后端目录 +cd "$BACKEND_DIR" || exit 1 + +# 安装后端依赖(仅生产依赖) +if [ -f "package.json" ]; then + echo "安装后端依赖..." + npm ci --production || npm install --production +fi + +# 初始化数据库(如果需要) +if [ -d "prisma" ]; then + echo "初始化数据库..." + # 检查数据库文件是否存在 + if [ ! -f "prisma/dev.db" ]; then + echo "数据库不存在,执行迁移..." + npx prisma migrate deploy || npx prisma migrate dev + npx prisma generate + # 如果有 seed 文件,执行 seed + if [ -f "prisma/seed.ts" ]; then + npx tsx prisma/seed.ts || echo "Seed 执行失败或已存在数据" + fi + else + echo "数据库已存在,执行迁移..." + npx prisma migrate deploy || npx prisma migrate dev + npx prisma generate + fi +fi + +# 设置文件权限 +echo "设置文件权限..." +chmod +x dist/index.js 2>/dev/null || true + +echo "部署脚本执行完成!" +echo "部署目录: $DEPLOY_DIR" +echo "前端文件: $DEPLOY_DIR" +echo "后端文件: $BACKEND_DIR"