feat: 添加生产环境部署配置 #5

Merged
moyin merged 1 commits from feature/deployment-config into main 2025-12-17 15:38:47 +08:00
8 changed files with 482 additions and 0 deletions
Showing only changes of commit a907e64f40 - Show all commits

20
.env.production.example Normal file
View File

@@ -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
# 其他配置
# 根据项目需要添加其他环境变量

5
.gitignore vendored
View File

@@ -11,6 +11,11 @@ build/
.env
.env.local
.env.*.local
.env.production
# 部署相关敏感文件
deploy.sh
webhook-handler.js
# 日志
*.log

217
DEPLOYMENT.md Normal file
View File

@@ -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 服务状态
- 验证数据库用户权限
- 检查网络连接

26
Dockerfile Normal file
View File

@@ -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"]

54
deploy.sh.example Normal file
View File

@@ -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 "警告:服务健康检查失败"

36
docker-compose.yml Normal file
View File

@@ -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:

38
ecosystem.config.js Normal file
View File

@@ -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
}
]
};

View File

@@ -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);
});
});