现在有这样一个需求。本地推送前端代码到 codeberg 后,服务端拉取代码,打包后,放到对应的位置。
服务器服务,全是由 docker 构建,文件复制移动全靠 volume 。
构建 api 接口服务
新建 webhook-server 目录,里边有下边几个文件。
.
├── Dockerfile
├── package.json
└── server.js
Dockerfile:
FROM node:latest
WORKDIR /app
# 新增:安装docker客户端
RUN apt-get update && apt-get install -y ca-certificates curl \
&& curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-27.3.1.tgz \
| tar -xz -C /usr/local/bin --strip-components=1 docker/docker \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 9000
CMD ["node", "server.js"]
package.json
{
"name": "webhook-server",
"version": "1.0.0",
"private": true,
"dependencies": {
"express": "^5.0"
}
}
server.js
/**
* Webhook 接收服务
*
* 接收 Codeberg (Gitea) 的 push 通知,验证签名后触发 deploy.sh
*/
const express = require('express');
const crypto = require('crypto');
const { spawn } = require('child_process');
const PORT = parseInt(process.env.WEBHOOK_PORT || '9000', 10);
const SECRET = process.env.WEBHOOK_SECRET || '';
const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT || '/opt/frontend-cicd/deploy.sh';
const TARGET_BRANCH = `refs/heads/${process.env.BRANCH || 'main'}`;
if (!SECRET || SECRET === 'CHANGE_ME') {
console.error('[ERROR] 请先设置 WEBHOOK_SECRET');
process.exit(1);
}
const app = express();
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook/deploy', (req, res) => {
const body = req.body.toString('utf8');
const sig = req.headers['x-gitea-signature'] || '';
const event = req.headers['x-gitea-event'] || '';
// 验证 HMAC-SHA256 签名
const expected = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
if (sig !== expected) {
console.warn(`[${ts()}] 签名验证失败`);
return res.status(403).json({ error: 'Invalid signature' });
}
if (event !== 'push') return res.json({ msg: `忽略事件: ${event}` });
let payload;
try { payload = JSON.parse(body); } catch { return res.status(400).end(); }
const ref = payload.ref || '';
if (ref !== TARGET_BRANCH) return res.json({ msg: `忽略分支: ${ref}` });
const pusher = payload.pusher?.login || '?';
const commits = (payload.commits || []).length;
console.log(`[${ts()}] ${pusher} 推送了 ${commits} 个提交到 ${ref},开始部署...`);
// 先返回 200,避免 Codeberg 超时
res.json({ msg: 'deploy triggered' });
console.log(`[${ts()}] 开始执行部署脚本: ${DEPLOY_SCRIPT}`);
const child = spawn('bash', [DEPLOY_SCRIPT], { timeout: 600000 });
child.stdout.on('data', (data) => {
console.log(`[${ts()}] [部署stdout] ${data.toString()}`);
});
child.stderr.on('data', (data) => {
console.error(`[${ts()}] [部署stderr] ${data.toString()}`);
});
child.on('close', (code) => {
if (code === 0) {
console.log(`[${ts()}] 部署完成,退出码:${code}`);
} else {
console.error(`[${ts()}] 部署失败,脚本退出码:${code}`);
}
});
child.on('error', (err) => {
console.error(`[${ts()}] 启动脚本异常:`, err);
});
});
app.get('/health', (_, res) => {
res.json({ ok: true, uptime: process.uptime(), time: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Webhook 服务已启动,端口 ${PORT}`);
console.log(` POST /webhook/deploy (分支: ${TARGET_BRANCH})`);
console.log(` GET /health`);
});
function ts() { return new Date().toISOString(); }
运行 api 接口服务
这里是完整的目录和文件。
.
├── deploy.sh
├── docker-compose.yml
├── setup.sh
└── webhook-server
├── Dockerfile
├── package.json
└── server.js
deploy.sh
#!/bin/bash
# ============================================================
# 自动部署脚本 —— 兼容 webhook docker 容器运行
# ============================================================
set -euo pipefail
# ======================== 配置(修改路径!) ========================
PROJECT_DIR="/alidata/code/editor.manhuariji.com"
DEPLOY_DIR="/alidata/www/editor.manhuariji.com/public"
DOCKER_IMAGE="xr-node:20"
CONTAINER_NAME="editor-mhrj"
BUILD_CMD="npm run build:h5"
BUILD_OUTPUT="dist"
DETAIL_OUTPUT="build/h5"
BRANCH="main"
# 【修改点1】日志放到容器内/app目录,拥有写入权限!
LOG_FILE="/app/editor-mhrj-deploy.log"
LOCK_FILE="/tmp/editor-mhrj-deploy.lock"
# =====================================================
# ---- 防并发 ----
if [ -f "$LOCK_FILE" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [WARN] 有部署正在运行,跳过" >> "$LOG_FILE"
exit 0
fi
touch "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
# log函数:同时输出到日志文件 AND 控制台(调试关键!)
log() {
local msg="$(date '+%Y-%m-%d %H:%M:%S') $1"
echo "$msg" >> "$LOG_FILE"
echo "$msg" # 打印到终端,Node服务可以捕获
}
log "========================================"
log "开始部署"
log "========================================"
# ---- 1. Git 拉取代码 ----
log "[1/3] cd $PROJECT_DIR && git pull ..."
cd "$PROJECT_DIR"
PREV=$(git rev-parse --short HEAD 2>/dev/null || echo "none")
# 强制同步远端代码,丢弃本地所有修改(适合自动部署服务器)
log "[INFO] 重置本地代码,对齐远端origin/${BRANCH}"
git fetch origin "${BRANCH}" >> "$LOG_FILE" 2>&1
git reset --hard "origin/${BRANCH}" >> "$LOG_FILE" 2>&1
NEW=$(git rev-parse --short HEAD)
#if [ "$PREV" = "$NEW" ]; then
# log "没有新提交 ($NEW),跳过构建"
# exit 0
# fi
log "代码更新: $PREV -> $NEW"
# ---- 2. Docker 容器内构建 ----
# 新增:清理宿主机缓存,腾出内存
log "[INFO] 清理系统缓存释放内存"
sync; echo 3 > /proc/sys/vm/drop_caches
log "[2/3] Docker 容器构建 ($DOCKER_IMAGE) ..."
log " npm install + $BUILD_CMD"
docker run --rm \
--memory=2G \
--memory-swap=3G \
--name "$CONTAINER_NAME" \
-v "${PROJECT_DIR}":/www \
-w /www \
"$DOCKER_IMAGE" \
bash -c "
npm config set registry https://registry.npmmirror.com
npm install
NODE_OPTIONS='--max-old-space-size=1400' npm run build:h5
" >> "$LOG_FILE" 2>&1
if [ $? -ne 0 ]; then
log "[ERROR] 构建失败!"
exit 1
fi
if [ ! -d "${PROJECT_DIR}/${BUILD_OUTPUT}" ]; then
log "[ERROR] 构建产物目录 '${BUILD_OUTPUT}' 不存在"
exit 1
fi
log "构建完成,产物: ${PROJECT_DIR}/${BUILD_OUTPUT}"
# ---- 3. 部署 ----
log "[3/3] 部署到 $DEPLOY_DIR ..."
# 备份逻辑保留(可选)
#if [ -d "$DEPLOY_DIR" ] && [ "$(ls -A "$DEPLOY_DIR" 2>/dev/null)" ]; then
# BACKUP="${DEPLOY_DIR}.backup"
# rm -rf "$BACKUP"
# mv "$DEPLOY_DIR" "$BACKUP"
# log "旧版本备份到 $BACKUP"
#fi
mkdir -p "$DEPLOY_DIR"
SRC="${PROJECT_DIR}/${BUILD_OUTPUT}/${DETAIL_OUTPUT}"
# 使用 cp -r 源头目录/. 方案,完美规避所有通配符问题
if [ -d "$SRC" ]; then
cp -r "${SRC}/." "${DEPLOY_DIR}/"
else
log "[ERROR] 构建目录不存在: $SRC"
exit 1
fi
log "========================================"
log "部署完成!($PREV -> $NEW)"
log "========================================"
# ---- 日志截断 ----
MAX_LINES=3000
if [ -f "$LOG_FILE" ]; then
LINES=$(wc -l < "$LOG_FILE")
if [ "$LINES" -gt "$MAX_LINES" ]; then
tail -n "$MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
mv "${LOG_FILE}.tmp" "$LOG_FILE"
fi
fi
bash.sh
#!/bin/bash
# ============================================================
# 一键初始化脚本
#
# 用法:
# 1. scp -r frontend-cicd root@服务器IP:/opt/
# 2. 编辑 /opt/frontend-cicd/.env(填仓库地址和 Secret)
# 3. bash /opt/frontend-cicd/setup.sh
# ============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/.env"
echo "=========================================="
echo " editor.manhuariji.com 自动部署初始化"
echo "=========================================="
# ---- 1. 检查环境 ----
echo ""
echo "[1/5] 检查环境 ..."
for cmd in git docker; do
if ! command -v $cmd &>/dev/null; then
echo "[ERROR] $cmd 未安装"
exit 1
fi
done
echo " git: $(git --version)"
echo " docker: $(docker --version)"
if [ "$WEBHOOK_SECRET" = "CHANGE_ME" ]; then
echo ""
echo "[ERROR] 请先编辑 .env,把 WEBHOOK_SECRET 改成随机字符串"
echo " 生成方法: openssl rand -hex 24"
exit 1
fi
# ---- 2. 克隆项目 ----
echo ""
echo "[2/5] 项目代码 ..."
if [ -d "$PROJECT_DIR/.git" ]; then
echo " 已存在,git pull ..."
cd "$PROJECT_DIR" && git pull
else
echo " 克隆 $REPO_URL ..."
mkdir -p "$(dirname "$PROJECT_DIR")"
git clone "$REPO_URL" "$PROJECT_DIR"
fi
# ---- 3. 首次构建验证 ----
echo ""
echo "[3/5] 首次构建验证 ..."
cd "$PROJECT_DIR"
docker run --rm \
--name "$CONTAINER_NAME" \
-v "${PROJECT_DIR}":/www \
-w /www \
"$DOCKER_IMAGE" \
bash -c "npm install && $BUILD_CMD"
if [ -d "$BUILD_OUTPUT" ]; then
mkdir -p "$(dirname "$DEPLOY_DIR")"
rm -rf "$DEPLOY_DIR"
mv "$BUILD_OUTPUT" "$DEPLOY_DIR"
echo " 首次部署完成 -> $DEPLOY_DIR"
else
echo "[WARN] 构建产物不存在,跳过部署"
fi
# ---- 4. 准备 Webhook 镜像依赖 ----
echo ""
echo "[4/5] 准备 Webhook 镜像 ..."
# 把宿主机的 docker CLI 复制到 webhook-server 目录,Dockerfile 会 COPY 进去
# 这样构建镜像时不需要拉任何外部镜像
cp "$(which docker)" "$SCRIPT_DIR/webhook-server/docker"
echo " 已复制 docker CLI -> webhook-server/docker"
# ---- 5. 启动 Webhook ----
echo ""
echo "[5/5] 启动 Webhook 服务 ..."
cd "$SCRIPT_DIR"
docker compose up -d --build
echo ""
echo "=========================================="
echo " 初始化完成!"
echo "=========================================="
echo ""
echo " Webhook: http://服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo " Secret: $WEBHOOK_SECRET"
echo ""
echo " 验证:"
echo " curl http://localhost:$WEBHOOK_PORT/health"
echo " docker logs editor-mhrj-webhook"
echo ""
echo " 下一步:"
echo " 1. 阿里云安全组放行 $WEBHOOK_PORT 端口"
echo " 2. Codeberg → Settings → Webhooks → Add Webhook (Gitea)"
echo " URL: http://你的服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo " Secret: $WEBHOOK_SECRET"
echo " Event: Push Events"
echo " 3. 本地 git push 验证"
echo ""
docker-compose.yaml
"docker-compose.yml" 25L, 851C 7,5 全部
services:
webhook-server:
build: ./webhook-server
container_name: editor-mhrj-webhook
restart: always
privileged: true
volumes:
- ./webhook-server/server.js:/app/server.js
# 挂载 docker.sock,让容器能调用宿主机的 docker 执行 deploy.sh
- /var/run/docker.sock:/var/run/docker.sock
- /root/.ssh:/root/.ssh:ro
# 挂载部署脚本(只读)
- ./deploy.sh:/app/deploy.sh:ro
- /alidata/code/editor.manhuariji.com:/alidata/code/editor.manhuariji.com
- /alidata/www/editor.manhuariji.com/public:/alidata/www/editor.manhuariji.com/public
environment:
- WEBHOOK_SECRET=${WEBHOOK_SECRET}
- WEBHOOK_PORT=${WEBHOOK_PORT:-9000}
- BRANCH=${BRANCH:-main}
- DEPLOY_SCRIPT=/app/deploy.sh
networks:
- xrnet
networks:
xrnet:
external: true
.env
# ======================== 按需修改 ========================
# Codeberg 仓库 SSH 地址(仅 setup.sh 首次 clone 用,deploy.sh 走已有的 remote)
REPO_URL=git@codeberg.org:manhuariji/manhuariji.h5.git
# 项目在宿主机上的路径
PROJECT_DIR=/alidata/code/editor.manhuariji.com
# 部署目标(Nginx public 目录)
DEPLOY_DIR=/alidata/www/editor.manhuariji.com/public
# 构建用的 Docker 镜像(你自己的镜像)
DOCKER_IMAGE=xr-node:20
# 容器名
CONTAINER_NAME=editor-mhrj
# 构建命令
BUILD_CMD=npm run build:h5
# 构建产物目录名
BUILD_OUTPUT=dist
# 打包输出详情目录
DETAIL_OUTPUT=build/h5/*
# 部署分支
BRANCH=main
# ---- WEBHOOK SECRET(和 git 无关!)----
# 这是 Codeberg 和你的 webhook 服务之间的"暗号",用来验证 HTTP 请求确实来自 Codeberg。
# 防止别人往你的 webhook 地址随便发 POST 触发部署。
# 生成方法:openssl rand -hex 24
# 这个值要和 Codeberg Webhook 设置页填的 Secret 一致。
# WEBHOOK_SECRET=CHANGE_ME
WEBHOOK_SECRET=b85cb03fd42507f5f921ac4534e76155c0045aed49d0236a
# Webhook 监听端口
WEBHOOK_PORT=9000
运行起来: docker compose up -d
nginx 代理
location /cicd/ {
proxy_pass http://webhook-server:9000/;
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_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Context /cicd;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location = /cicd {
return 301 /cicd/;
}
然后通过域名就可以开始部署了: https://xxx.com/cicd/webhook/deploy
codeberg 配置 Forgejo
登录 codeberg,进入项目,点击设置,切换到 web钩子页签。再点击添加web钩子, 选择 Forgejo。开始进行配置。

密钥文件和服务器上的文件一致哈。
可以点击 测试推送进行测试,也可以主动推送代码进行测试。
