轻量级Gitee Webhook + Docker自动化部署方案(Java项目实战)
适合中小团队、个人开发者的极简CI/CD方案,内存占用<25MB,不依赖Jenkins/GitLab Runner等重型工具。
一、方案背景与选型
1.1 需求
- 代码提交到Gitee企业版master分支后,自动拉取代码、Maven打包、部署到Docker容器
- 服务器资源有限,要求尽可能轻量
- 线上已有稳定运行的Redis/MySQL/EMQX等服务,不能动
- 只需要更新两个Spring Boot服务:
admin(已有容器)和shop(新增)
1.2 为什么不用Jenkins/GitHub Actions?
| 方案 | 内存占用 | 复杂度 | 依赖 |
|---|---|---|---|
| Jenkins | 500MB+ | 高,需要JDK+插件 | JDK、大量插件 |
| GitLab Runner | 200MB+ | 中 | 需要注册Runner |
| 本方案 | 15~25MB | 极低 | Python3(系统自带)、Git、Maven、Docker |
本方案核心:一个Python标准库HTTP服务器 + 一个Shell部署脚本,零额外Python依赖(不装Flask/Django),全部代码不到500行。
1.3 架构图
开发者 push 代码到 Gitee master 分支
│
▼
┌─────────────────────────────┐
│ Gitee Webhook (Push事件) │
│ POST http://服务器IP:9000/webhook │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ webhook_listener.py │ ← Python3标准库HTTPServer
│ 端口9000,内存~20MB │ ← 无第三方依赖
│ 1.验证Token │
│ 2.解析commits判断变更服务 │
│ 3.异步线程执行deploy.sh │
└─────────────┬───────────────┘
│ subprocess异步调用
▼
┌─────────────────────────────┐
│ deploy.sh (Shell脚本) │
│ 1. git pull 拉取最新代码 │ ← HTTPS+Token认证,非交互
│ 2. mvn package 增量打包 │ ← 智能判断只构建变更模块
│ 3. backup 备份旧jar │ ← 保留7天,可回滚
│ 4. cp 新jar到部署目录 │
│ 5. docker restart 重启容器 │ ← 原生命令,不依赖compose
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ Docker 容器 │
│ - java (admin:8080) │ ← 原有容器直接重启
│ - java-shop (shop:8085) │ ← 首次自动docker run创建
└─────────────────────────────┘
二、服务器环境准备
2.1 必要软件
bash
# 检查环境(CentOS/Ubuntu通用)
python3 --version # 需要Python 3.6+,CentOS7自带Python3.6
git --version
mvn -version # Maven 3.6+
docker --version
docker-compose --version # 可选,本方案不强依赖
2.2 目录规划
/var/data/
├── docker/
│ └── deploy/ # 部署脚本目录
│ ├── webhook_listener.py
│ ├── deploy.sh
│ ├── deploy.conf
│ ├── manual-deploy.sh
│ ├── start.sh
│ ├── stop.sh
│ ├── webhook-listener.service
│ ├── webhook.log
│ ├── deploy.log
│ └── shop_cloud_admin/ # Git代码克隆目录
└── java/ # Jar包部署目录(容器挂载点)
├── admin.jar
├── libtaos.so
├── uploadPath/
├── logs/
└── shop/
├── shop.jar
└── logs/
三、核心代码实现
3.1 Webhook监听服务(webhook_listener.py)
使用Python标准库 http.server 实现,不安装任何pip包:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
轻量级Webhook监听服务 - 使用Python标准库,无需额外依赖
监听Gitee Webhook,触发自动部署脚本
"""
import os
import sys
import json
import hmac
import subprocess
import threading
import logging
from http.server import HTTPServer, BaseHTTPRequestHandler
from logging.handlers import RotatingFileHandler
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(BASE_DIR, 'deploy.conf')
LOG_FILE = os.path.join(BASE_DIR, 'webhook.log')
DEPLOY_LOCK = os.path.join(BASE_DIR, '.deploy.lock')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
RotatingFileHandler(LOG_FILE, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def load_config():
"""加载配置文件,支持key=value格式"""
config = {
'port': 9000,
'secret': '',
'deploy_script': os.path.join(BASE_DIR, 'deploy.sh'),
'project_dir': '/var/data/docker/deploy/shop_cloud_admin',
'branches': ['master', 'main']
}
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key == 'port':
config[key] = int(value)
elif key == 'branches':
config[key] = [b.strip() for b in value.split(',')]
else:
config[key] = value
except Exception as e:
logger.error(f'加载配置文件失败: {e}')
return config
CONFIG = load_config()
def verify_token(token, secret):
"""验证Gitee Webhook密码,使用hmac.compare_digest防时序攻击"""
if not secret:
return True
if not token:
return False
try:
return hmac.compare_digest(str(token), str(secret))
except Exception as e:
logger.error(f'Token验证失败: {e}')
return False
def run_deploy(branch, service):
"""异步执行部署脚本,通过文件锁防并发"""
if os.path.exists(DEPLOY_LOCK):
logger.warning('部署正在进行中,跳过本次触发')
return
try:
with open(DEPLOY_LOCK, 'w') as f:
f.write(str(os.getpid()))
logger.info(f'开始部署 - 分支: {branch}, 服务: {service}')
env = os.environ.copy()
env['DEPLOY_BRANCH'] = branch
env['DEPLOY_SERVICE'] = service if service else 'all'
env['PROJECT_DIR'] = CONFIG['project_dir']
proc = subprocess.Popen(
['bash', CONFIG['deploy_script']],
cwd=BASE_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
for line in proc.stdout:
logger.info(f'[deploy] {line.strip()}')
proc.wait()
if proc.returncode == 0:
logger.info('部署成功完成')
else:
logger.error(f'部署失败,退出码: {proc.returncode}')
except Exception as e:
logger.error(f'部署执行异常: {e}')
finally:
if os.path.exists(DEPLOY_LOCK):
os.remove(DEPLOY_LOCK)
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
"""处理Gitee Webhook POST请求"""
if self.path != '/webhook':
self.send_error(404)
return
content_length = int(self.headers.get('Content-Length', 0))
payload = self.rfile.read(content_length)
# Token验证
gitee_token = self.headers.get('X-Gitee-Token', '')
if CONFIG.get('secret') and not verify_token(gitee_token, CONFIG['secret']):
logger.warning('Token验证失败,拒绝请求')
self.send_response(403)
self.end_headers()
self.wfile.write(b'Forbidden')
return
try:
event = json.loads(payload.decode('utf-8'))
except json.JSONDecodeError:
self.send_response(400)
self.end_headers()
self.wfile.write(b'Bad Request')
return
# 解析分支
ref = event.get('ref', '')
branch = ref.replace('refs/heads/', '') if ref.startswith('refs/heads/') else ref
if not branch:
logger.warning('无法获取分支信息')
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
return
# 只处理配置的分支
valid_branches = CONFIG.get('branches', ['master'])
if branch not in valid_branches:
logger.info(f'分支 {branch} 不在部署列表中,跳过')
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
return
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
# 智能判断需要部署哪些服务
service = ''
commits = event.get('commits', [])
if commits:
modified_files = []
for commit in commits:
modified_files.extend(commit.get('modified', []))
modified_files.extend(commit.get('added', []))
has_admin = any(f.startswith('springboot/admin/') for f in modified_files)
has_shop = any(f.startswith('springboot/shop/') for f in modified_files)
has_common = any(
f.startswith('springboot/')
for f in modified_files
if not f.startswith('springboot/admin/')
and not f.startswith('springboot/shop/')
)
if has_common or (has_admin and has_shop):
service = 'all' # 公共模块变更,全量构建
elif has_admin:
service = 'admin' # 只改了admin
elif has_shop:
service = 'shop' # 只改了shop
logger.info(f'收到Webhook - 分支: {branch}, 变更服务: {service or "all"}')
# 异步线程执行,不阻塞HTTP响应
t = threading.Thread(target=run_deploy, args=(branch, service))
t.daemon = True
t.start()
def do_GET(self):
"""健康检查端点"""
if self.path == '/health':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
status = 'deploying' if os.path.exists(DEPLOY_LOCK) else 'idle'
self.wfile.write(json.dumps({'status': status}).encode())
else:
self.send_response(200)
self.end_headers()
self.wfile.write(b'Webhook listener is running')
def log_message(self, format, *args):
logger.info('%s - %s', self.address_string(), format % args)
def main():
port = CONFIG.get('port', 9000)
server = HTTPServer(('0.0.0.0', port), WebhookHandler)
logger.info(f'Webhook监听服务启动,端口: {port}')
logger.info(f'部署脚本: {CONFIG["deploy_script"]}')
logger.info(f'项目目录: {CONFIG["project_dir"]}')
logger.info(f'监听分支: {CONFIG["branches"]}')
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info('服务停止')
server.shutdown()
if __name__ == '__main__':
main()
关键设计点:
- 文件锁防并发 :
.deploy.lock文件防止多次push同时触发部署 - 智能增量部署:根据commits中修改的文件路径,判断只构建admin/shop还是全量构建(公共模块如common/entity变更时全量构建)
- 异步执行:Webhook请求立即返回200,部署在后台线程执行,避免Gitee超时重试
- 日志轮转 :使用
RotatingFileHandler,单文件10MB,保留5个备份,不撑满磁盘 - Token验证:支持Gitee Webhook密码验证,防止恶意请求
3.2 部署脚本(deploy.sh)
bash
#!/bin/bash
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_FILE="$DEPLOY_DIR/deploy.conf"
LOG_FILE="$DEPLOY_DIR/deploy.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
load_config() {
# 默认配置
PROJECT_DIR="/var/data/docker/deploy/shop_cloud_admin"
JAVA_DEPLOY_DIR="/var/data/java"
MAVEN_CMD="mvn"
GIT_REPO_URL=""
GIT_USERNAME=""
GIT_PASSWORD=""
BRANCH="${DEPLOY_BRANCH:-master}"
SERVICE="${DEPLOY_SERVICE:-all}"
ADMIN_CONTAINER="java"
SHOP_CONTAINER="java-shop"
SHOP_PORT="8085"
NETWORK_NAME=""
# 从配置文件读取
if [ -f "$CONFIG_FILE" ]; then
while IFS='=' read -r key value || [ -n "$key" ]; do
key=$(echo "$key" | tr -d '[:space:]')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
case "$key" in
project_dir) PROJECT_DIR="$value" ;;
java_deploy_dir) JAVA_DEPLOY_DIR="$value" ;;
maven_cmd) MAVEN_CMD="$value" ;;
git_repo_url) GIT_REPO_URL="$value" ;;
git_username) GIT_USERNAME="$value" ;;
git_password) GIT_PASSWORD="$value" ;;
admin_container) ADMIN_CONTAINER="$value" ;;
shop_container) SHOP_CONTAINER="$value" ;;
shop_port) SHOP_PORT="$value" ;;
network_name) NETWORK_NAME="$value" ;;
esac
done < "$CONFIG_FILE"
fi
SPRINGBOOT_DIR="$PROJECT_DIR/springboot"
# 自动检测Docker网络(从已有的admin容器获取)
if [ -z "$NETWORK_NAME" ]; then
NETWORK_NAME=$(docker inspect "$ADMIN_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' 2>/dev/null || echo "")
if [ -z "$NETWORK_NAME" ]; then
NETWORK_NAME=$(docker network ls --format '{{.Name}}' | grep -v 'bridge\|host\|none' | head -1)
fi
fi
log "使用Docker网络: ${NETWORK_NAME:-默认bridge}"
}
init_git() {
# 关键:禁用Git交互式密码提示!
export GIT_TERMINAL_PROMPT=0
export GIT_ASKPASS=echo
if [ ! -d "$PROJECT_DIR" ]; then
# 首次克隆
log "项目目录不存在,创建目录..."
mkdir -p "$(dirname "$PROJECT_DIR")"
local clone_url="$GIT_REPO_URL"
if [ -n "$GIT_USERNAME" ] && [ -n "$GIT_PASSWORD" ]; then
# URL中嵌入认证信息: https://user:pass@gitee.com/xxx.git
clone_url=$(echo "$clone_url" | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|")
fi
log "首次克隆代码仓库..."
git clone -b "$BRANCH" "$clone_url" "$PROJECT_DIR"
else
cd "$PROJECT_DIR"
# 更新已有仓库的remote URL(防止密码变更)
if [ -d "$PROJECT_DIR/.git" ] && [ -n "$GIT_USERNAME" ] && [ -n "$GIT_PASSWORD" ]; then
local remote_url=$(git remote get-url origin 2>/dev/null || echo "")
if [ -z "$remote_url" ]; then
remote_url="$GIT_REPO_URL"
fi
local clean_url=$(echo "$remote_url" | sed -E 's|https://[^@]+@|https://|')
local authed_url=$(echo "$clean_url" | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|")
git remote set-url origin "$authed_url" 2>/dev/null || true
fi
fi
cd "$PROJECT_DIR"
log "拉取最新代码 (分支: $BRANCH)..."
git fetch origin
git checkout "$BRANCH"
git reset --hard "origin/$BRANCH"
git pull origin "$BRANCH"
log "代码拉取完成,当前commit: $(git rev-parse --short HEAD)"
}
backup_jar() {
"""备份旧jar,保留7天"""
local jar_name=$1
local jar_path="$JAVA_DEPLOY_DIR/$jar_name"
if [ -f "$jar_path" ]; then
local backup_path="$jar_path.backup.$(date +%Y%m%d%H%M%S)"
log "备份旧版本: $jar_name -> $(basename "$backup_path")"
cp "$jar_path" "$backup_path"
find "$JAVA_DEPLOY_DIR" -maxdepth 2 -name "${jar_name}.backup.*" -type f -mtime +7 -delete 2>/dev/null || true
fi
}
container_exists() {
docker ps -a --format '{{.Names}}' | grep -q "^${1}$"
}
container_running() {
docker ps --format '{{.Names}}' | grep -q "^${1}$"
}
build_admin() {
"""构建并部署admin"""
log "========== 开始构建 admin =========="
cd "$SPRINGBOOT_DIR"
log "执行Maven打包 (admin)..."
# -pl 指定模块,-am 同时构建依赖模块,-DskipTests跳过测试,-q静默模式
$MAVEN_CMD clean package -pl admin -am -DskipTests -q
local jar_file="$SPRINGBOOT_DIR/admin/target/admin.jar"
if [ ! -f "$jar_file" ]; then
log "ERROR: admin.jar 构建失败,文件不存在"
return 1
fi
local jar_size=$(du -h "$jar_file" | cut -f1)
log "admin.jar 构建成功,大小: $jar_size"
backup_jar "admin.jar"
cp "$jar_file" "$JAVA_DEPLOY_DIR/admin.jar"
log "admin.jar 已部署到 $JAVA_DEPLOY_DIR/"
# 直接用docker restart,不依赖docker-compose!
if container_running "$ADMIN_CONTAINER"; then
log "重启容器: $ADMIN_CONTAINER"
docker restart "$ADMIN_CONTAINER"
elif container_exists "$ADMIN_CONTAINER"; then
log "启动已停止的容器: $ADMIN_CONTAINER"
docker start "$ADMIN_CONTAINER"
else
log "ERROR: 容器 $ADMIN_CONTAINER 不存在,请先手动创建"
return 1
fi
sleep 8
if container_running("$ADMIN_CONTAINER"); then
log "admin 容器重启成功"
else
log "WARNING: 请检查日志: docker logs $ADMIN_CONTAINER"
fi
log "========== admin 部署完成 =========="
}
build_shop() {
"""构建并部署shop,首次自动创建容器"""
log "========== 开始构建 shop =========="
mkdir -p "$JAVA_DEPLOY_DIR/shop/logs"
mkdir -p "$JAVA_DEPLOY_DIR/uploadPath"
cd "$SPRINGBOOT_DIR"
log "执行Maven打包 (shop)..."
$MAVEN_CMD clean package -pl shop -am -DskipTests -q
local jar_file="$SPRINGBOOT_DIR/shop/target/shop.jar"
if [ ! -f "$jar_file" ]; then
log "ERROR: shop.jar 构建失败"
return 1
fi
local jar_size=$(du -h "$jar_file" | cut -f1)
log "shop.jar 构建成功,大小: $jar_size"
backup_jar "shop/shop.jar"
cp "$jar_file" "$JAVA_DEPLOY_DIR/shop/shop.jar"
log "shop.jar 已部署到 $JAVA_DEPLOY_DIR/shop/"
if container_running "$SHOP_CONTAINER"; then
log "重启容器: $SHOP_CONTAINER"
docker restart "$SHOP_CONTAINER"
elif container_exists "$SHOP_CONTAINER"; then
log "启动已停止的容器: $SHOP_CONTAINER"
docker start "$SHOP_CONTAINER"
else
# 首次部署:自动docker run创建容器,加入和admin相同的网络
log "java-shop 容器不存在,首次创建并启动..."
local network_arg=""
if [ -n "$NETWORK_NAME" ]; then
network_arg="--network $NETWORK_NAME"
fi
docker run -d \
--name "$SHOP_CONTAINER" \
$network_arg \
-p ${SHOP_PORT}:${SHOP_PORT} \
--privileged \
-v "$JAVA_DEPLOY_DIR/shop/shop.jar:/server.jar" \
-v "$JAVA_DEPLOY_DIR/uploadPath:/uploadPath" \
-v "$JAVA_DEPLOY_DIR/shop/logs:/logs" \
-v /etc/localtime:/etc/localtime \
-e TZ=Asia/Shanghai \
--restart=always \
openjdk:8-jre \
java -jar /server.jar
log "$SHOP_CONTAINER 容器已创建并启动"
fi
sleep 8
if container_running "$SHOP_CONTAINER"; then
log "shop 容器启动成功"
else
log "WARNING: 请检查日志: docker logs $SHOP_CONTAINER"
fi
log "========== shop 部署完成 =========="
}
main() {
log "=========================================="
log "开始自动部署,目标服务: $SERVICE"
log "=========================================="
load_config
init_git
mkdir -p "$JAVA_DEPLOY_DIR" "$JAVA_DEPLOY_DIR/shop/logs" "$JAVA_DEPLOY_DIR/uploadPath"
case "$SERVICE" in
admin) build_admin ;;
shop) build_shop ;;
all) build_admin; build_shop ;;
*) log "未知服务,部署全部"; build_admin; build_shop ;;
esac
log "=========================================="
log "所有部署任务完成"
log "=========================================="
}
main
关键踩坑与解决:
- Git非交互认证 :
GIT_TERMINAL_PROMPT=0禁用交互式提示,URL中嵌入用户名:Token@认证 - 不用docker-compose :服务器上的compose文件可能有未完成的服务定义导致校验失败,直接用
docker restart/docker run原生命令 - 网络自动检测 :新容器通过
docker inspect获取已有admin容器所在网络,保证能访问Redis/MySQL - 增量Maven构建 :
-pl admin -am只构建指定模块及其依赖,不用全量构建
3.3 配置文件(deploy.conf)
bash
# Webhook监听端口
port=9000
# Gitee Webhook密钥(在Gitee Webhook设置中配置的密码)
secret=your_webhook_secret_here
# Git仓库地址(原始URL,不带账号密码)
git_repo_url=https://gitee.com/your-company/your-repo.git
# Git认证(Gitee手机号/邮箱 + 私人访问令牌)
git_username=your_phone_number
git_password=your_gitee_private_token
# 服务器上代码存放目录
project_dir=/var/data/docker/deploy/shop_cloud_admin
# Jar包部署目录(与Docker容器挂载路径一致)
java_deploy_dir=/var/data/java
# Maven命令路径
maven_cmd=mvn
# Docker容器名称
admin_container=java
shop_container=java-shop
# Shop服务端口
shop_port=8085
# Docker网络(留空自动检测)
network_name=
# 触发部署的分支(逗号分隔)
branches=master
3.4 辅助脚本
手动部署脚本(manual-deploy.sh):
bash
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE="${1:-all}"
echo "手动触发部署,服务: $SERVICE"
cd "$SCRIPT_DIR"
DEPLOY_SERVICE="$SERVICE" bash deploy.sh
启动脚本(start.sh):
bash
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PID_FILE="$SCRIPT_DIR/webhook.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "Webhook服务已在运行 (PID: $PID)"
exit 1
fi
fi
cd "$SCRIPT_DIR"
nohup python3 "$SCRIPT_DIR/webhook_listener.py" >> "$SCRIPT_DIR/webhook-nohup.log" 2>&1 &
echo $! > "$PID_FILE"
echo "Webhook服务已启动 (PID: $!)"
echo "日志: tail -f $SCRIPT_DIR/webhook.log"
停止脚本(stop.sh):
bash
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PID_FILE="$SCRIPT_DIR/webhook.pid"
if [ ! -f "$PID_FILE" ]; then
echo "PID文件不存在,尝试查找进程..."
pkill -f "webhook_listener.py" && echo "已停止" || echo "未找到运行进程"
exit 0
fi
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill "$PID"
sleep 1
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID"
fi
echo "Webhook服务已停止 (PID: $PID)"
else
echo "进程 $PID 未运行"
fi
rm -f "$PID_FILE"
systemd服务配置(webhook-listener.service,用于开机自启):
ini
[Unit]
Description=FastBee Auto Deploy Webhook Listener
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
User=root
WorkingDirectory=/var/data/docker/deploy
ExecStart=/usr/bin/python3 /var/data/docker/deploy/webhook_listener.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=webhook-listener
[Install]
WantedBy=multi-user.target
四、服务器部署步骤
4.1 上传文件
将 docker/deploy/ 目录下所有文件上传到服务器 /var/data/docker/deploy/:
bash
# 服务器上执行
mkdir -p /var/data/docker/deploy
cd /var/data/docker/deploy
# 用scp/rz等方式上传文件后:
chmod +x *.sh
4.2 修改配置
bash
vi /var/data/docker/deploy/deploy.conf
重点修改:
git_repo_url:你的Gitee仓库地址git_username:Gitee手机号git_password:Gitee私人令牌(在Gitee → 设置 → 私人令牌 生成)secret:Webhook密码(自定义字符串)java_deploy_dir:确认jar包挂载路径(执行docker inspect java | grep -A5 Mounts查看)
4.3 确认容器挂载路径
bash
# 查看java容器的挂载情况
docker inspect java --format='{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
确保输出中有 /var/data/java/admin.jar -> /server.jar,如果路径不同则修改 java_deploy_dir。
4.4 获取Gitee私人令牌
Gitee → 右上角头像 → 设置 → 私人令牌 → 生成新令牌(勾选 projects 权限即可)。
4.5 首次手动部署测试
bash
cd /var/data/docker/deploy
./manual-deploy.sh all
观察输出日志,确认:
- git clone/pull 成功
- Maven打包成功(输出jar大小)
- jar复制到部署目录成功
- docker restart 成功
- 容器正常运行(
docker ps | grep java)
4.6 启动Webhook服务
方式一:快速启动(先用这个)
bash
cd /var/data/docker/deploy
./start.sh
验证:
bash
curl http://localhost:9000/health
# 返回: {"status": "idle"}
方式二:systemd开机自启(推荐生产环境)
bash
cp /var/data/docker/deploy/webhook-listener.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable webhook-listener
systemctl start webhook-listener
systemctl status webhook-listener
五、Gitee Webhook配置
进入Gitee企业版仓库 → 管理 → WebHooks → 添加WebHook:
| 配置项 | 值 |
|---|---|
| URL | http://你的服务器公网IP:9000/webhook |
| 密码 | 与 deploy.conf 中 secret 一致 |
| 触发事件 | 勾选 Push |
| 分支 | 选择 master |
⚠️ 防火墙配置 :服务器安全组/防火墙需要开放 9000端口 入站。
配置完成后点击"测试"按钮,Gitee会发送一条测试请求,检查服务器日志:
bash
tail -f /var/data/docker/deploy/webhook.log
六、docker-compose.yml 调整(新增shop服务)
如果需要用compose管理shop服务(可选,因为脚本已支持自动docker run),在原有compose文件中java服务后添加:
yaml
java-shop:
image: openjdk:8-jre
container_name: java-shop
ports:
- 8085:8085
privileged: true
networks:
network:
ipv4_address: 177.7.0.17
depends_on:
- emqx
- redis
- mysql
volumes:
- /var/data/java/shop/shop.jar:/server.jar
- /var/data/java/uploadPath:/uploadPath
- /var/data/java/shop/logs:/logs
- /etc/localtime:/etc/localtime
environment:
TZ: Asia/Shanghai
entrypoint: java -jar /server.jar
restart: always
注意 :如果线上compose文件有其他服务依赖问题(如缺少zkc-gateway定义),建议不要修改compose文件,让脚本用
docker run自动创建即可。
七、常用运维命令
bash
# ===== 服务管理 =====
./start.sh # 启动Webhook
./stop.sh # 停止Webhook
systemctl status webhook-listener # systemd方式查看状态
# ===== 日志查看 =====
tail -f webhook.log # Webhook监听日志
tail -f deploy.log # 部署执行日志
tail -f /var/data/java/logs/xxx.log # 应用日志
docker logs -f java # admin容器日志
docker logs -f java-shop # shop容器日志
# ===== 手动部署 =====
./manual-deploy.sh all # 部署全部
./manual-deploy.sh admin # 仅部署admin
./manual-deploy.sh shop # 仅部署shop
# ===== 容器管理 =====
docker ps | grep java # 查看Java容器状态
docker restart java # 手动重启admin
docker restart java-shop # 手动重启shop
# ===== 健康检查 =====
curl http://localhost:9000/health
# ===== 回滚 =====
# 备份文件格式: admin.jar.backup.YYYYMMDDHHMMSS
cd /var/data/java
cp admin.jar.backup.20260626142808 admin.jar
docker restart java
八、踩坑记录与经验总结
坑1:Git交互式输入密码挂起
现象 :fatal: could not read Username for 'https://gitee.com': Input/output error
原因 :非交互式环境下Git弹出用户名密码输入框,没有终端就报错
解决:
bash
export GIT_TERMINAL_PROMPT=0 # 禁用交互提示
# URL中直接嵌入认证信息
git clone https://用户名:Token@gitee.com/xxx/repo.git
坑2:docker-compose命令报错"no configuration file provided"
现象 :docker-compose restart java 报错找不到配置文件或invalid compose project
原因:
- 新版Docker Compose不会自动从当前目录查找配置文件
- 线上compose文件可能有未完成的服务定义(如依赖了不存在的服务),导致compose命令校验整个文件失败
解决 :直接用docker restart原生命令,不依赖compose文件。只操作单个容器时不需要走compose。
坑3:日志重复输出
现象 :deploy.sh中的日志每行打印两次
原因 :log() 函数用了 tee -a,同时main函数又做了 2>&1 重定向导致重复
解决:去掉多余的重定向,log函数统一处理输出。
坑4:Maven全量构建太慢
解决 :使用 -pl 模块名 -am 参数只构建指定模块及其依赖:
bash
mvn clean package -pl admin -am -DskipTests -q
-pl admin:只构建admin模块-am(also-make):同时构建所依赖的模块(如common模块)-DskipTests:跳过测试-q:静默模式,减少输出
坑5:并发部署冲突
现象 :快速连续push两次代码导致两个部署进程同时运行,Maven构建冲突
解决 :文件锁机制 .deploy.lock,部署前检查锁文件存在则跳过,部署完成后删除。
九、扩展建议
- 构建缓存:Maven本地仓库已有缓存,后续构建会快很多
- 通知机制:可在deploy.sh末尾加curl调用企业微信/钉钉机器人Webhook,部署完成发通知
- 健康检查:部署后curl接口检查应用是否真的启动成功,失败则自动回滚
- 多环境:通过不同分支触发不同环境部署(dev→测试服,master→正式服)
- 前端部署:同样的思路可扩展到Vue/React前端,加一个npm build + nginx重载逻辑即可
本方案在阿里云CentOS服务器上验证通过,稳定运行中。整体内存占用约20MB(Python进程),磁盘占用主要来自代码和Maven缓存,是个人/小团队自动化部署的轻量选择。