From a907e64f40aa832bfdf903bb88c468a5caa7e2bc Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 17 Dec 2025 15:37:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Dockerfile 和 docker-compose.yml 支持容器化部署 - 添加 PM2 配置文件 ecosystem.config.js - 添加部署脚本模板 deploy.sh.example - 添加 Gitea webhook 处理器模板 webhook-handler.js.example - 添加生产环境配置模板 .env.production.example - 添加详细的部署指南 DEPLOYMENT.md - 更新 .gitignore 排除敏感配置文件 --- .env.production.example | 20 ++++ .gitignore | 5 + DEPLOYMENT.md | 217 +++++++++++++++++++++++++++++++++++++ Dockerfile | 26 +++++ deploy.sh.example | 54 +++++++++ docker-compose.yml | 36 ++++++ ecosystem.config.js | 38 +++++++ webhook-handler.js.example | 86 +++++++++++++++ 8 files changed, 482 insertions(+) create mode 100644 .env.production.example create mode 100644 DEPLOYMENT.md create mode 100644 Dockerfile create mode 100644 deploy.sh.example create mode 100644 docker-compose.yml create mode 100644 ecosystem.config.js create mode 100644 webhook-handler.js.example diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..b88f43c --- /dev/null +++ b/.env.production.example @@ -0,0 +1,20 @@ +# 生产环境配置模板 +# 复制此文件为 .env.production 并填入实际值 + +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=your_db_username +DB_PASSWORD=your_db_password +DB_NAME=your_db_name + +# 应用配置 +NODE_ENV=production +PORT=3000 + +# JWT 配置(如果有的话) +JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters +JWT_EXPIRES_IN=7d + +# 其他配置 +# 根据项目需要添加其他环境变量 \ No newline at end of file diff --git a/.gitignore b/.gitignore index e110325..0629034 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,11 @@ build/ .env .env.local .env.*.local +.env.production + +# 部署相关敏感文件 +deploy.sh +webhook-handler.js # 日志 *.log diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..06c6431 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,217 @@ +# 部署指南 + +本文档详细说明如何部署 Pixel Game Server 到生产环境。 + +## 前置要求 + +- Node.js 18+ +- pnpm 包管理器 +- MySQL 8.0+ +- PM2 进程管理器(推荐) +- Nginx(可选,用于反向代理) + +## 部署步骤 + +### 1. 服务器环境准备 + +```bash +# 安装 Node.js (使用 NodeSource 仓库) +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs + +# 安装 pnpm +curl -fsSL https://get.pnpm.io/install.sh | sh +source ~/.bashrc + +# 安装 PM2 +npm install -g pm2 + +# 安装 MySQL +sudo apt update +sudo apt install mysql-server +sudo mysql_secure_installation +``` + +### 2. 克隆项目 + +```bash +# 创建项目目录 +sudo mkdir -p /var/www +cd /var/www + +# 克隆项目(替换为你的实际仓库地址) +sudo git clone https://your-gitea-server.com/username/pixel-game-server.git +sudo chown -R $USER:$USER pixel-game-server +cd pixel-game-server +``` + +### 3. 配置环境 + +```bash +# 复制环境配置文件 +cp .env.production.example .env.production + +# 编辑环境配置(填入实际的数据库信息) +nano .env.production + +# 复制部署脚本 +cp deploy.sh.example deploy.sh +chmod +x deploy.sh + +# 编辑部署脚本(修改路径配置) +nano deploy.sh + +# 复制 webhook 处理器 +cp webhook-handler.js.example webhook-handler.js + +# 编辑 webhook 处理器(修改密钥和路径) +nano webhook-handler.js +``` + +### 4. 数据库设置 + +```bash +# 登录 MySQL +sudo mysql -u root -p + +# 创建数据库和用户 +CREATE DATABASE pixel_game_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'pixel_game'@'localhost' IDENTIFIED BY 'your_secure_password'; +GRANT ALL PRIVILEGES ON pixel_game_db.* TO 'pixel_game'@'localhost'; +FLUSH PRIVILEGES; +EXIT; +``` + +### 5. 安装依赖和构建 + +```bash +# 安装依赖 +pnpm install --frozen-lockfile + +# 构建项目 +pnpm run build +``` + +### 6. 启动服务 + +```bash +# 使用 PM2 启动应用 +pm2 start ecosystem.config.js --env production + +# 保存 PM2 配置 +pm2 save + +# 设置开机自启 +pm2 startup +# 按照提示执行显示的命令 +``` + +### 7. 配置 Nginx(可选) + +创建 Nginx 配置文件: + +```bash +sudo nano /etc/nginx/sites-available/pixel-game-server +``` + +添加以下内容: + +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3000; + 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; + } + + location /webhook { + proxy_pass http://localhost:9000; + 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; + } +} +``` + +启用站点: + +```bash +sudo ln -s /etc/nginx/sites-available/pixel-game-server /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +``` + +## Gitea Webhook 配置 + +1. 在 Gitea 仓库中进入 **Settings** → **Webhooks** +2. 点击 **Add Webhook** → **Gitea** +3. 配置: + - **Target URL**: `http://your-server.com:9000/webhook` 或 `http://your-domain.com/webhook` + - **HTTP Method**: `POST` + - **POST Content Type**: `application/json` + - **Secret**: 与 `webhook-handler.js` 中的 `SECRET` 一致 + - **Trigger On**: 选择 `Push events` + - **Branch filter**: `main` + +## 验证部署 + +```bash +# 检查服务状态 +pm2 status + +# 查看日志 +pm2 logs pixel-game-server +pm2 logs webhook-handler + +# 测试 API +curl http://localhost:3000/ +curl http://localhost:3000/api-docs +``` + +## 常用命令 + +```bash +# 重启服务 +pm2 restart pixel-game-server + +# 查看日志 +pm2 logs pixel-game-server --lines 100 + +# 手动部署 +bash deploy.sh + +# 更新代码(不重启) +git pull origin main +pnpm install +pnpm run build +pm2 reload pixel-game-server +``` + +## 故障排除 + +### 服务无法启动 +- 检查环境变量配置 +- 检查数据库连接 +- 查看 PM2 日志 + +### Webhook 不工作 +- 检查防火墙设置 +- 验证 webhook URL 可访问性 +- 检查 Gitea webhook 日志 +- 验证签名密钥是否一致 + +### 数据库连接失败 +- 检查 MySQL 服务状态 +- 验证数据库用户权限 +- 检查网络连接 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..772b078 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# 使用官方 Node.js 镜像 +FROM node:18-alpine + +# 设置工作目录 +WORKDIR /app + +# 安装 pnpm +RUN npm install -g pnpm + +# 复制 package.json 和 pnpm-lock.yaml +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +# 安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源代码 +COPY . . + +# 构建应用 +RUN pnpm run build + +# 暴露端口 +EXPOSE 3000 + +# 启动应用 +CMD ["pnpm", "run", "start:prod"] \ No newline at end of file diff --git a/deploy.sh.example b/deploy.sh.example new file mode 100644 index 0000000..7e97101 --- /dev/null +++ b/deploy.sh.example @@ -0,0 +1,54 @@ +#!/bin/bash + +# 部署脚本模板 - 用于 Gitea Webhook 自动部署 +# 复制此文件为 deploy.sh 并根据服务器环境修改配置 +set -e + +echo "开始部署 Pixel Game Server..." + +# 项目路径(根据你的服务器实际路径修改) +PROJECT_PATH="/var/www/pixel-game-server" +BACKUP_PATH="/var/backups/pixel-game-server" + +# 创建备份 +echo "创建备份..." +mkdir -p $BACKUP_PATH +cp -r $PROJECT_PATH $BACKUP_PATH/backup-$(date +%Y%m%d-%H%M%S) + +# 进入项目目录 +cd $PROJECT_PATH + +# 拉取最新代码 +echo "拉取最新代码..." +git pull origin main + +# 安装/更新依赖 +echo "安装依赖..." +pnpm install --frozen-lockfile + +# 构建项目 +echo "构建项目..." +pnpm run build + +# 重启服务 +echo "重启服务..." +if command -v pm2 &> /dev/null; then + # 使用 PM2 + pm2 restart pixel-game-server || pm2 start dist/main.js --name pixel-game-server +elif command -v docker-compose &> /dev/null; then + # 使用 Docker Compose + docker-compose down + docker-compose up -d --build +else + # 使用 systemd + sudo systemctl restart pixel-game-server +fi + +echo "部署完成!" + +# 清理旧备份(保留最近5个) +find $BACKUP_PATH -maxdepth 1 -type d -name "backup-*" | sort -r | tail -n +6 | xargs rm -rf + +echo "服务状态检查..." +sleep 5 +curl -f http://localhost:3000/health || echo "警告:服务健康检查失败" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..957b459 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3.8' + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USERNAME=pixel_game + - DB_PASSWORD=your_password + - DB_NAME=pixel_game_db + depends_on: + - mysql + restart: unless-stopped + volumes: + - ./logs:/app/logs + + mysql: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=root_password + - MYSQL_DATABASE=pixel_game_db + - MYSQL_USER=pixel_game + - MYSQL_PASSWORD=your_password + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + restart: unless-stopped + +volumes: + mysql_data: \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..49691da --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,38 @@ +module.exports = { + apps: [ + { + name: 'pixel-game-server', + script: 'dist/main.js', + instances: 1, + exec_mode: 'cluster', + env: { + NODE_ENV: 'production', + PORT: 3000 + }, + env_production: { + NODE_ENV: 'production', + PORT: 3000 + }, + log_file: './logs/combined.log', + out_file: './logs/out.log', + error_file: './logs/error.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + max_memory_restart: '1G', + restart_delay: 4000, + watch: false, + ignore_watch: ['node_modules', 'logs'] + }, + { + name: 'webhook-handler', + script: 'webhook-handler.js', + instances: 1, + env: { + NODE_ENV: 'production', + PORT: 9000 + }, + restart_delay: 4000, + watch: false + } + ] +}; \ No newline at end of file diff --git a/webhook-handler.js.example b/webhook-handler.js.example new file mode 100644 index 0000000..3a97c12 --- /dev/null +++ b/webhook-handler.js.example @@ -0,0 +1,86 @@ +const http = require('http'); +const crypto = require('crypto'); +const { exec } = require('child_process'); + +// 配置 - 复制此文件为 webhook-handler.js 并修改配置 +const PORT = 9000; +const SECRET = 'your_webhook_secret_change_this'; // 与 Gitea 中配置的密钥一致 +const DEPLOY_SCRIPT = '/var/www/pixel-game-server/deploy.sh'; // 修改为实际路径 + +// 验证 Gitea 签名 +function verifySignature(payload, signature, secret) { + const hmac = crypto.createHmac('sha256', secret); + hmac.update(payload); + const calculatedSignature = hmac.digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(signature, 'hex'), + Buffer.from(calculatedSignature, 'hex') + ); +} + +// 创建 HTTP 服务器 +const server = http.createServer((req, res) => { + if (req.method !== 'POST') { + res.writeHead(405, { 'Content-Type': 'text/plain' }); + res.end('Method Not Allowed'); + return; + } + + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + + req.on('end', () => { + try { + // 验证签名 + const signature = req.headers['x-gitea-signature']; + if (!signature || !verifySignature(body, signature.replace('sha256=', ''), SECRET)) { + console.log('签名验证失败'); + res.writeHead(401, { 'Content-Type': 'text/plain' }); + res.end('Unauthorized'); + return; + } + + const payload = JSON.parse(body); + + // 检查是否是推送到 main 分支 + if (payload.ref === 'refs/heads/main') { + console.log('收到 main 分支推送,开始部署...'); + + // 执行部署脚本 + exec(`bash ${DEPLOY_SCRIPT}`, (error, stdout, stderr) => { + if (error) { + console.error('部署失败:', error); + console.error('stderr:', stderr); + } else { + console.log('部署成功:', stdout); + } + }); + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Deployment triggered'); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Not main branch, ignored'); + } + } catch (error) { + console.error('处理 webhook 失败:', error); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } + }); +}); + +server.listen(PORT, () => { + console.log(`Webhook 处理器运行在端口 ${PORT}`); +}); + +// 优雅关闭 +process.on('SIGTERM', () => { + console.log('收到 SIGTERM,正在关闭服务器...'); + server.close(() => { + console.log('服务器已关闭'); + process.exit(0); + }); +}); \ No newline at end of file