Files
BYW-VoiceHub/patches/0001-chore-deploy-Docker-compose-Gitea-Actions.patch
T

459 lines
17 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
From 171854222838fa3c5f01bef96ffc9b820bc4bf00 Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Sun, 6 Sep 2026 08:23:39 +0800
Subject: [PATCH 01/58] =?UTF-8?q?chore(deploy):=20=E4=BF=AE=E5=A4=8D=20Doc?=
=?UTF-8?q?ker=20=E5=90=AF=E5=8A=A8=E9=93=BE=20+=20=E8=A1=A5=20compose=20+?=
=?UTF-8?q?=20Gitea=20Actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitea/workflows/deploy.yml | 164 ++++++++++++++++++++++++++++++++
docker-compose.yml | 182 ++++++++++++++++++++++++++++--------
scripts/deploy.js | 30 +++---
3 files changed, 323 insertions(+), 53 deletions(-)
create mode 100644 .gitea/workflows/deploy.yml
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..ca3da1f
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,164 @@
+name: 构建并部署 VoiceHub
+run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}}
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ # 允许在 Gitea 页面手动点「运行工作流」
+ workflow_dispatch:
+ inputs:
+ deploy_target:
+ description: 是否在构建后自动部署到服务器
+ type: boolean
+ default: true
+ required: false
+
+# 同一时刻只跑一个部署,避免部署竞态
+concurrency:
+ group: deploy-production
+ cancel-in-progress: false
+
+env:
+ # ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ======
+ GITEA_REGISTRY: gitea.example.com
+ # ====== 一般不改 ======
+ IMAGE_NAME: byw-voicehub
+ DEPLOY_PATH: /opt/byw-voicehub
+ NODE_VERSION: "22"
+
+jobs:
+ # ============================================================
+ # Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库
+ # ============================================================
+ build-image:
+ name: 构建 Docker 镜像
+ runs-on: ubuntu-latest
+ outputs:
+ image_tag: ${{ steps.meta.outputs.tag }}
+ image_full: ${{ steps.meta.outputs.full }}
+ steps:
+ - name: 📥 拉取代码
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: 🔧 设置 Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ registry-url: https://registry.npmmirror.com # 国内加速,不需要可删
+
+ - name: 🧪 环境校验
+ run: |
+ set -euxo pipefail
+ node -v
+ npm -v
+ npm ci --no-audit --no-fund
+
+ - name: 🔐 登录 Gitea 容器仓库
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITEA_REGISTRY }}
+ username: ${{ gitea.repository_owner }}
+ password: ${{ secrets.REGISTRY_TOKEN || gitea.token }}
+
+ - name: 🏷️ 生成镜像标签
+ id: meta
+ run: |
+ set -euo pipefail
+ SHORT_SHA="${GITEA_SHA:0:7}"
+ DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
+ FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
+ LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
+ echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT"
+ echo "full=${FULL}" >> "$GITEA_OUTPUT"
+ echo "latest=${LATEST}" >> "$GITEA_OUTPUT"
+ echo "✅ 镜像 tag: ${DATE_TAG}"
+ echo "✅ 镜像完整: ${FULL}"
+
+ - name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存)
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: |
+ ${{ steps.meta.outputs.full }}
+ ${{ steps.meta.outputs.latest }}
+ cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
+ cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
+ build-args: |
+ BUILDKIT_INLINE_CACHE=1
+ labels: |
+ org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
+ org.opencontainers.image.revision=${{ gitea.sha }}
+
+ # ============================================================
+ # Job 2: SSH 到服务器拉取新镜像并重启
+ # ============================================================
+ deploy:
+ name: 部署到服务器
+ needs: build-image
+ if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }}
+ runs-on: ubuntu-latest
+ environment: production
+ steps:
+ - name: 🌐 SSH 部署到生产服务器
+ uses: https://github.com/appleboy/ssh-action@v1.2.0
+ with:
+ host: ${{ secrets.DEPLOY_HOST }}
+ username: ${{ secrets.DEPLOY_USER }}
+ key: ${{ secrets.DEPLOY_SSH_KEY }}
+ port: ${{ secrets.DEPLOY_PORT || 22 }}
+ script_stop: true
+ command_timeout: 30m
+ script: |
+ set -euxo pipefail
+
+ # 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env
+ mkdir -p "${DEPLOY_PATH}"
+ cd "${DEPLOY_PATH}"
+
+ # 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全)
+ echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \
+ | docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true
+
+ # 3) 拉最新镜像
+ IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
+ docker pull "$IMAGE_LATEST"
+
+ # 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像
+ # (避免改动服务器上已配置好的 docker-compose.yml 和 .env)
+ cat > docker-compose.override.yml <<'YAML'
+ services:
+ voicehub:
+ image: __REPLACE_WITH_IMAGE__
+ YAML
+ sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml
+
+ # 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份)
+ if [ ! -f docker-compose.yml ]; then
+ echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)"
+ curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \
+ -o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)"
+ fi
+
+ # 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小)
+ docker compose pull --quiet voicehub 2>/dev/null || true
+ docker compose up -d --remove-orphans
+
+ # 7) 等健康检查通过,最多等 120s
+ for i in $(seq 1 24); do
+ if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
+ echo "✅ 容器健康"
+ break
+ fi
+ sleep 5
+ done
+
+ # 8) 清理旧镜像(只留最近 2 个 tag,省磁盘)
+ docker image prune -af --filter "until=72h" || true
+
+ echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"
diff --git a/docker-compose.yml b/docker-compose.yml
index c1ed74d..f89810e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,57 +1,159 @@
+name: BYW VoiceHub (1.7.0.0)
services:
+ # ========================================================================
+ # 应用本体 - Nuxt 4 (Node SSR)
+ # - 使用两阶段构建 Dockerfile,构建阶段内存友好
+ # - PREBUILT=true:告诉 scripts/deploy.js 跳过 npm install 和 nuxt build
+ # (因为第一阶段已经 build 好了,产物在 .output/ 里)
+ # ========================================================================
voicehub:
- build: .
+ image: byw-voicehub:1.7.0.0
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ BUILDKIT_INLINE_CACHE: "1"
+ container_name: voicehub-app
+ restart: unless-stopped
ports:
- - '4456:3000'
- environment:
- - DATABASE_URL=postgresql://user:password@postgres:5432/voicehub
- - JWT_SECRET=your-jwt-secret-here
- - NODE_ENV=production
- - NODE_OPTIONS=--max-old-space-size=512
- - SKIP_BUILD=true
- mem_limit: 1200m
- mem_reservation: 256m
- cpus: '1.0'
- deploy:
- resources:
- limits:
- cpus: '1'
- memory: 1200M
- reservations:
- cpus: '0.25'
- memory: 256M
+ # 左边是宿主机端口(对外),右边是容器内端口(固定 3000)
+ # 如果要套 Nginx,就只监听 127.0.0.1:4456:3000 不直接暴露外网
+ - "4456:3000"
depends_on:
postgres:
condition: service_healthy
- restart: unless-stopped
-
- postgres:
- image: postgres:15-alpine
+ redis:
+ condition: service_healthy
+ env_file:
+ # 把密码、密钥都写在项目根的 .env 里,不要直接写进这个 yaml
+ - .env
environment:
- - POSTGRES_USER=user
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=voicehub
- volumes:
- - postgres_data:/var/lib/postgresql/data
- ports:
- - '5433:5432'
- mem_limit: 1200m
- mem_reservation: 256m
- cpus: '1.0'
+ # ===== 运行时基础(可以被 .env 覆盖) =====
+ NODE_ENV: production
+ PORT: 3000
+ HOST: 0.0.0.0
+ PREBUILT: "true"
+ SKIP_INSTALL: "true"
+ SKIP_BUILD: "true"
+ NITRO_PRESET: node-server
+
+ # ===== 数据库连接串(.env 写了就用 .env,没写就自动拼 compose 内部 postgres 服务) =====
+ DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-voicehub}:${POSTGRES_PASSWORD:-voicehub}@postgres:5432/${POSTGRES_DB:-voicehub}?sslmode=disable}
+
+ # ===== Redis 连接串(同上,自动拼内部 redis 服务) =====
+ REDIS_URL: ${REDIS_URL:-redis://${REDIS_PASSWORD:+:${REDIS_PASSWORD}@}redis:6379/0}
+
+ # ===== 会话/Token(必须在 .env 里设,这里给一个占位提醒) =====
+ JWT_SECRET: ${JWT_SECRET:-请在.env里设置JWT_SECRET用openssl rand -hex 32生成}
+
+ # ===== 应用默认(可在 .env 覆盖,不会覆盖 .env 里同名字段) =====
+ NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-http://127.0.0.1:4456}
+ NUXT_PUBLIC_SITE_TITLE: ${NUXT_PUBLIC_SITE_TITLE:-校园广播站点歌系统}
+ NUXT_PUBLIC_DEFAULT_THEME: ${NUXT_PUBLIC_DEFAULT_THEME:-dark}
+ NUXT_PUBLIC_ENABLED_THEMES: ${NUXT_PUBLIC_ENABLED_THEMES:-dark,light}
+
+ # ===== 日志/性能 =====
+ NODE_OPTIONS: ${NODE_OPTIONS:-"--max-old-space-size=768"}
+ # 2C4G 机器的推荐配置(.env 可覆盖);小机器 1C2G 也能跑
+ mem_limit: ${APP_MEM_LIMIT:-1400m}
+ mem_reservation: ${APP_MEM_RESERVATION:-384m}
+ cpus: ${APP_CPUS:-"1.5"}
deploy:
resources:
limits:
- cpus: '1'
- memory: 1200M
+ cpus: ${APP_CPUS_LIMIT:-"2"}
+ memory: ${APP_MEM_LIMIT:-1400M}
reservations:
- cpus: '0.25'
- memory: 256M
+ cpus: ${APP_CPUS_RESERVATION:-"0.5"}
+ memory: ${APP_MEM_RESERVATION:-384M}
+ healthcheck:
+ # 30s 后开始检查,每 15s 一次,连续 3 次失败就重启
+ test:
+ [
+ "CMD",
+ "node",
+ "-e",
+ "fetch('http://127.0.0.1:3000/').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"
+ ]
+ interval: 15s
+ timeout: 5s
+ retries: 3
+ start_period: 30s
+ networks:
+ - voicehub-net
+ volumes:
+ # (可选)把自定义 logo/上传资源挂到宿主机
+ - voicehub-uploads:/app/app/public/uploads:rw
+
+ # ========================================================================
+ # PostgreSQL 16 - 主数据库
+ # ========================================================================
+ postgres:
+ image: postgres:16-alpine
+ container_name: voicehub-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER:-voicehub}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-请在.env里改这个密码}
+ POSTGRES_DB: ${POSTGRES_DB:-voicehub}
+ PGDATA: /var/lib/postgresql/data/pgdata
+ # .env 里没设 DATABASE_URL 时,自动拼一份给 app 用
+ # (如果 .env 里已经写了外部 Postgres,就以 .env 为准)
healthcheck:
- test: ['CMD-SHELL', 'pg_isready -U user -d voicehub']
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-voicehub} -d ${POSTGRES_DB:-voicehub}"]
interval: 5s
timeout: 5s
- retries: 5
+ retries: 10
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ # 如需从宿主机用 Navicat 连,取消下一行注释
+ # ports:
+ # - "127.0.0.1:5433:5432"
+ mem_limit: ${PG_MEM_LIMIT:-1200m}
+ mem_reservation: ${PG_MEM_RESERVATION:-256m}
+ cpus: ${PG_CPUS:-"1.0"}
+ networks:
+ - voicehub-net
+
+ # ========================================================================
+ # Redis 7 - 缓存 / 会话 / 限流 / 队列
+ # ========================================================================
+ redis:
+ image: redis:7-alpine
+ container_name: voicehub-redis
restart: unless-stopped
+ command: >
+ sh -c "
+ if [ -n \"$$REDIS_PASSWORD\" ]; then
+ redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\"
+ else
+ redis-server --appendonly yes
+ fi
+ "
+ environment:
+ REDIS_PASSWORD: ${REDIS_PASSWORD:-}
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 3s
+ retries: 10
+ volumes:
+ - redis-data:/data
+ mem_limit: ${REDIS_MEM_LIMIT:-384m}
+ mem_reservation: ${REDIS_MEM_RESERVATION:-64m}
+ cpus: ${REDIS_CPUS:-"0.5"}
+ networks:
+ - voicehub-net
volumes:
- postgres_data:
+ postgres-data:
+ name: voicehub-postgres-data
+ redis-data:
+ name: voicehub-redis-data
+ voicehub-uploads:
+ name: voicehub-uploads
+
+networks:
+ voicehub-net:
+ name: voicehub-net
+ driver: bridge
diff --git a/scripts/deploy.js b/scripts/deploy.js
index c20219f..9963ca4 100644
--- a/scripts/deploy.js
+++ b/scripts/deploy.js
@@ -106,26 +106,26 @@ async function deploy() {
logStep('📦', '安装依赖...')
let installed = false
- // 优先尝试 pnpm install --frozen-lockfile
- if (fileExists('pnpm-lock.yaml')) {
- log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
- if (safeExec('pnpm install --frozen-lockfile')) {
+ // 优先使用 npm(用户要求:禁止 pnpm)—— 优先 package-lock.json 冻结安装
+ if (fileExists('package-lock.json')) {
+ log('尝试使用 npm ci(基于 package-lock.json 冻结安装)...', 'cyan')
+ if (safeExec('npm ci --no-audit --no-fund')) {
installed = true
- logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
+ logSuccess('依赖安装完成 (npm ci)')
} else {
- logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
+ logWarning('npm ci 失败,准备回退到 npm install...')
}
} else {
- logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
+ logWarning('未检测到 package-lock.json,直接使用 npm install...')
}
- // 如果冻结锁文件安装没运行或失败,使用 pnpm install
+ // 兜底用 npm install
if (!installed) {
- log('正在使用 pnpm install 安装...', 'cyan')
- if (!safeExec('pnpm install')) {
+ log('正在使用 npm install 安装...', 'cyan')
+ if (!safeExec('npm install --no-audit --no-fund')) {
throw new Error('依赖安装失败')
}
- logSuccess('依赖安装完成 (pnpm install)')
+ logSuccess('依赖安装完成 (npm install)')
}
}
@@ -166,7 +166,10 @@ async function deploy() {
// 4. 创建管理员账户
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
logStep('👤', '检查管理员账户...')
- safeExec('pnpm run create-admin')
+ // 优先 npm run create-admin,兜底直接用 node_modules/.bin/tsx 调用(用户禁用 pnpm)
+ if (!safeExec('npm run create-admin')) {
+ safeExec('./node_modules/.bin/tsx scripts/create-admin.js')
+ }
}
// 5. 构建应用
@@ -178,7 +181,8 @@ async function deploy() {
...process.env,
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
}
- if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
+ // 用 npm run build(= nuxt build),避免用 pnpm exec
+ if (!safeExec('npm run build', { env: buildEnv })) {
throw new Error('应用构建失败')
}
logSuccess('应用构建完成')
--
2.45.1.windows.1