feat: live module and jenkins pipeline to harbor deploy

This commit is contained in:
Yvan
2026-09-21 16:45:44 +08:00
parent 451c8e2abc
commit 829d75c657
145 changed files with 8984 additions and 269 deletions
+118 -220
View File
@@ -1,262 +1,160 @@
// live.fuhua91.com 部署流水线(基于 Jenkinsfile2 模板改造)
// 构建前后端镜像推送 Harbor(harbor.fuhua91.com/live),并 SSH 到宝塔服务器(192.168.2.226)
// 执行 /root/deploy.sh 拉取镜像更新容器。
//
// Jenkins 凭据要求:
// - harbor-live-push : Username with password,Harbor 项目 live 的 robot 账号
// (robot$live+live / harbor.json 中的 secret)
// - deploy-server-ssh-key: SSH Username with private key,可登录 root@192.168.2.226
//
// 服务器要求: /root/deploy.sh 已就位(见 deploy/deploy.sh),首次已手动跑通
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
}
parameters {
choice(
name: 'DEPLOY_MODE',
choices: ['BUILD_AND_DEPLOY', 'DEPLOY_ONLY'],
description: 'BUILD_AND_DEPLOY 构建新镜像并部署;DEPLOY_ONLY 部署 Harbor 中已有的镜像 tag(回滚用)'
)
string(
name: 'IMAGE_TAG',
defaultValue: '',
description: '要部署的镜像 tag。DEPLOY_ONLY 必填;BUILD_AND_DEPLOY 留空则使用当前 commit'
)
}
environment {
// ══════════════════════════════════════════════════════════════════
// 项目级参数 —— 基于 kra 模板的新项目只需修改本块。
// 这些变量自动 export 到 shell,由 envsubst 白名单渲染进 k8s 清单。
// ══════════════════════════════════════════════════════════════════
APP_NAME = 'kra' // 派生:服务名/Secret名/库名/存储声明/镜像后缀
IMAGE_REPO = 'library/kra' // Harbor 中镜像仓库路径
REGISTRY = '192.168.200.132'
BIN_NAME = 'kratos-admin' // Makefile build 的产物名
// 当前单节点部署固定使用单副本,避免首次初始化和 hostPath 数据共享产生竞争。
KRA_REPLICAS = '1'
// 中间件按环境开关;关闭时对应工作负载缩容到 0,不创建运行 Pod。
KRA_DEPLOY_MYSQL = 'true'
KRA_DEPLOY_REDIS = 'true'
KRA_FRONTEND_NODEPORT = '30080'
KRA_BACKEND_NODEPORT = '30081'
KRA_MYSQL_IMAGE = 'docker.m.daocloud.io/library/mysql:8.4'
KRA_REDIS_IMAGE = 'docker.m.daocloud.io/library/redis:7-alpine'
GO_IMAGE = 'docker.m.daocloud.io/golang:1.25'
NODE_IMAGE = 'docker.m.daocloud.io/node:20-slim'
PNPM_VERSION = '10.15.1'
CACHE_DIR = '/var/lib/kra-ci-cache' // Jenkins 宿主机依赖缓存根目录
// Jenkins 凭据 ID(新项目若沿用同一套凭据可不改)
CRED_HARBOR_PUSH = 'harbor-library-push'
CRED_KUBECONFIG_DEV = 'kra-rke2-128-kubeconfig'
CRED_KUBECONFIG_PROD = 'kra-rke2-129-kubeconfig'
CRED_MYSQL_PASSWORD = 'kra-mysql-root-password'
CRED_JWT_SIGNING_KEY = 'kra-jwt-signing-key'
// k8s 清单渲染白名单:只替换列出的变量,避免误伤清单内 shell 变量(如 probe 里的 $MYSQL_ROOT_PASSWORD)
K8S_RENDER_VARS = '$KRA_APP $KRA_NAMESPACE $KRA_ENV $KRA_BACKEND_IMAGE $KRA_FRONTEND_IMAGE $KRA_REPLICAS $KRA_DEPLOY_MYSQL $KRA_DEPLOY_REDIS $KRA_MYSQL_REPLICAS $KRA_REDIS_REPLICAS $KRA_MYSQL_WAIT_IMAGE $KRA_FRONTEND_NODEPORT $KRA_BACKEND_NODEPORT $KRA_MYSQL_IMAGE $KRA_REDIS_IMAGE $BIN_NAME'
APP_NAME = 'live'
REGISTRY_HOST = 'harbor.fuhua91.com'
REGISTRY_NAMESPACE = 'live'
BACKEND_IMAGE = "${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/backend"
FRONTEND_IMAGE = "${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/frontend"
DEPLOY_HOST = 'root@192.168.2.226'
BIN_NAME = 'kratos-admin'
APP_BASE = 'live' // 前端 vite base 与 nginx 服务名前缀
PNPM_VERSION = '10.15.1'
DOCKER_BUILDKIT = '1'
}
stages {
stage('Select environment') {
stage('Checkout') {
steps {
script {
// 分支来源优先级:
// 1) env.gitlabBranch —— GitLab Jenkins 集成/webhook 触发时由 gitlab-plugin 注入(push payload 的 ref)
// 2) env.BRANCH_NAME —— 多分支流水线场景
// 3) JOB_NAME —— 手动触发兜底:名字以 -dev 结尾按 dev 部署,否则默认 main
def branch = env.gitlabBranch ?: env.BRANCH_NAME
if (!branch) {
branch = env.JOB_NAME.endsWith('-dev') ? 'dev' : 'main'
}
branch = branch.trim().replaceFirst(/^refs\/heads\//, '')
// The backend NodePort service has the longest generated name:
// <APP_NAME>-backend-nodeport, which must stay within 63 characters.
if (!(env.APP_NAME ==~ /^[a-z0-9](?:[a-z0-9-]{0,44}[a-z0-9])?$/)) {
error("APP_NAME must be a lowercase Kubernetes name with at most 46 characters: ${env.APP_NAME}")
}
echo "gitlabBranch='${env.gitlabBranch ?: ''}' BRANCH_NAME='${env.BRANCH_NAME ?: ''}' -> resolved branch='${branch}'"
env.KRA_BRANCH = branch
if (branch == 'dev') {
env.KRA_NAMESPACE = "${env.APP_NAME}-dev"
env.KRA_ENV = 'development'
env.KUBECONFIG_CREDENTIALS = env.CRED_KUBECONFIG_DEV
} else if (branch == 'main') {
env.KRA_NAMESPACE = "${env.APP_NAME}-prod"
env.KRA_ENV = 'production'
env.KUBECONFIG_CREDENTIALS = env.CRED_KUBECONFIG_PROD
} else {
error("Unsupported branch: ${branch}. Only dev and main deploy.")
}
}
checkout scm
}
}
stage('Checkout triggered commit') {
stage('Prepare') {
steps {
// job 的 SCM branch spec 固定(如 */main),集成触发时按实际推送分支重新检出,
// 保证单 job 同时服务 dev/main 两个环境。
script {
// gitlabAfter 为 push 事件的目标 commit;手动触发时为空。
// 注意:不能把 null 赋给 env.*(会被转成字符串 "null"),这里用局部变量。
def triggerCommit = env.gitlabAfter?.trim()
if (triggerCommit && !(triggerCommit ==~ /[0-9a-fA-F]{40}/)) {
error("Invalid GitLab commit SHA: ${triggerCommit}")
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
env.VERSION = sh(script: 'git describe --tags --always --dirty', returnStdout: true).trim()
if (params.DEPLOY_MODE == 'DEPLOY_ONLY') {
if (!params.IMAGE_TAG?.trim()) {
error('IMAGE_TAG is required when DEPLOY_MODE=DEPLOY_ONLY')
}
env.DEPLOY_IMAGE_TAG = params.IMAGE_TAG.trim()
} else {
env.DEPLOY_IMAGE_TAG = params.IMAGE_TAG?.trim() ? params.IMAGE_TAG.trim() : env.GIT_COMMIT_SHORT
}
if (triggerCommit && triggerCommit.replace('0', '').isEmpty()) {
error('GitLab event points to a deleted branch; refusing to deploy')
}
if (triggerCommit) {
env.KRA_TRIGGER_COMMIT = triggerCommit
}
checkout([$class: 'GitSCM',
branches: [[name: "*/${env.KRA_BRANCH}"]],
userRemoteConfigs: [[credentialsId: 'gitlab-root-http', url: 'http://192.168.200.130/root/kra.git']]])
if (env.KRA_TRIGGER_COMMIT) {
sh "git fetch --no-tags origin ${env.KRA_TRIGGER_COMMIT}"
sh "git checkout --detach ${env.KRA_TRIGGER_COMMIT}"
}
env.KRA_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
if (env.KRA_TRIGGER_COMMIT && env.KRA_COMMIT != env.KRA_TRIGGER_COMMIT) {
error("Checked out ${env.KRA_COMMIT}, expected GitLab commit ${env.KRA_TRIGGER_COMMIT}")
}
echo "building commit ${env.KRA_COMMIT}"
echo "Deploy mode: ${params.DEPLOY_MODE}"
echo "Backend : ${env.BACKEND_IMAGE}:${env.DEPLOY_IMAGE_TAG}"
echo "Frontend: ${env.FRONTEND_IMAGE}:${env.DEPLOY_IMAGE_TAG}"
}
}
}
stage('Test') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
sh '''
set -eu
HOST_WORKSPACE=$(printf '%s' "$WORKSPACE" | sed 's#/var/jenkins_home#/opt/jenkins#')
docker run --rm -e GOPROXY=https://goproxy.cn,direct \\
-v "$HOST_WORKSPACE:/src" \\
-v "$CACHE_DIR/go-mod:/go/pkg/mod" \\
-v "$CACHE_DIR/go-build:/root/.cache/go-build" \\
-w /src "$GO_IMAGE" sh -c "git config --global --add safe.directory /src && go test ./... && make build"
'''
sh '''
set -eu
HOST_WORKSPACE=$(printf '%s' "$WORKSPACE" | sed 's#/var/jenkins_home#/opt/jenkins#')
docker run --rm \\
-e COREPACK_NPM_REGISTRY=https://registry.npmmirror.com \\
-v "$HOST_WORKSPACE/web:/app" \\
-v "$CACHE_DIR/pnpm-store:/root/.local/share/pnpm/store" \\
-v "$CACHE_DIR/corepack:/root/.cache/node/corepack" \\
-w /app "$NODE_IMAGE" bash -lc "corepack enable && corepack prepare pnpm@$PNPM_VERSION --activate && pnpm install --frozen-lockfile && pnpm build"
# 前端 Nginx 配置模板渲染(proxy_pass 指向 ${APP_NAME}-backend 服务)。
# KRA_APP 必须在此显式导出:envsubst 会把未设置的变量替换为空串。
export KRA_APP="$APP_NAME"
envsubst '$KRA_APP' < web/.docker-compose/nginx/conf.d/my.conf > web/nginx.k8s.conf
go env -w GOPROXY=https://goproxy.cn,direct
go test ./...
'''
}
}
stage('Build and push images') {
stage('Build Frontend') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
script {
env.IMAGE_TAG = "${env.KRA_BRANCH}-${env.KRA_COMMIT.take(12)}"
}
withCredentials([usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) {
// 容器内构建 dist(vite base 由 web/.env.production 的 VITE_APP_BASE=/admin/ 控制),
// 并渲染前端 nginx 配置(/api 转发到 live-backend:8000)
sh '''
docker run --rm \
-v "$PWD/web:/app" -w /app \
-e COREPACK_NPM_REGISTRY=https://registry.npmmirror.com \
-e npm_config_registry=https://registry.npmmirror.com \
-v kra-pnpm-store:/root/.local/share/pnpm/store \
docker.m.daocloud.io/node:20-slim \
bash -lc "corepack enable && corepack prepare pnpm@$PNPM_VERSION --activate && pnpm install --frozen-lockfile && pnpm build"
sed "s/\\${KRA_APP}/$APP_BASE/g" web/.docker-compose/nginx/conf.d/my.conf > web/nginx.k8s.conf
'''
}
}
stage('Build Images') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
sh '''
CGO_ENABLED=0 go build -ldflags "-X main.Version=${VERSION}" -o bin/${BIN_NAME} ./cmd
docker build \
--build-arg BIN_NAME=${BIN_NAME} \
--tag ${BACKEND_IMAGE}:${DEPLOY_IMAGE_TAG} \
--tag ${BACKEND_IMAGE}:latest \
.
docker build \
--tag ${FRONTEND_IMAGE}:${DEPLOY_IMAGE_TAG} \
--tag ${FRONTEND_IMAGE}:latest \
web
'''
}
}
stage('Push Images') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
withCredentials([usernamePassword(credentialsId: 'harbor-live-push', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS')]) {
sh '''
set -eu
umask 077
DOCKER_CONFIG_DIR=$(mktemp -d)
trap 'rm -rf "$DOCKER_CONFIG_DIR"' EXIT
echo "$HARBOR_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login "$REGISTRY" --username "$HARBOR_USER" --password-stdin >/dev/null
docker --config "$DOCKER_CONFIG_DIR" build --build-arg BIN_NAME="$BIN_NAME" -t "$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG" .
docker --config "$DOCKER_CONFIG_DIR" build -t "$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG" web
docker --config "$DOCKER_CONFIG_DIR" push "$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG"
docker --config "$DOCKER_CONFIG_DIR" push "$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG"
docker --config "$DOCKER_CONFIG_DIR" logout "$REGISTRY" >/dev/null 2>&1 || true
echo "${HARBOR_PASS}" | docker login "${REGISTRY_HOST}" -u "${HARBOR_USER}" --password-stdin
docker push ${BACKEND_IMAGE}:${DEPLOY_IMAGE_TAG}
docker push ${BACKEND_IMAGE}:latest
docker push ${FRONTEND_IMAGE}:${DEPLOY_IMAGE_TAG}
docker push ${FRONTEND_IMAGE}:latest
docker logout "${REGISTRY_HOST}"
'''
}
}
}
stage('Deploy to Rancher cluster') {
stage('Deploy') {
steps {
script {
if (!(env.KRA_DEPLOY_MYSQL in ['true', 'false'])) {
error("KRA_DEPLOY_MYSQL must be true or false: ${env.KRA_DEPLOY_MYSQL}")
}
if (!(env.KRA_DEPLOY_REDIS in ['true', 'false'])) {
error("KRA_DEPLOY_REDIS must be true or false: ${env.KRA_DEPLOY_REDIS}")
}
def credentialBindings = [
file(credentialsId: "${env.KUBECONFIG_CREDENTIALS}", variable: 'KUBECONFIG_FILE'),
string(credentialsId: env.CRED_JWT_SIGNING_KEY, variable: 'KRA_JWT_SIGNING_KEY'),
usernamePassword(credentialsId: env.CRED_HARBOR_PUSH, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')
]
if (env.KRA_DEPLOY_MYSQL == 'true') {
credentialBindings << string(credentialsId: env.CRED_MYSQL_PASSWORD, variable: 'KRA_MYSQL_ROOT_PASSWORD')
}
withCredentials(credentialBindings) {
sh '''
set -eu
export KUBECONFIG="$KUBECONFIG_FILE"
umask 077
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
kubectl create namespace "$KRA_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
DOCKER_CONFIG_DIR="$TMP_DIR/docker"
mkdir -p "$DOCKER_CONFIG_DIR"
printf '%s' "$HARBOR_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login "$REGISTRY" --username "$HARBOR_USER" --password-stdin >/dev/null
kubectl -n "$KRA_NAMESPACE" create secret generic harbor-registry \\
--type=kubernetes.io/dockerconfigjson \\
--from-file=.dockerconfigjson="$DOCKER_CONFIG_DIR/config.json" \\
--dry-run=client -o yaml | kubectl apply -f -
docker --config "$DOCKER_CONFIG_DIR" logout "$REGISTRY" >/dev/null 2>&1 || true
if [ "$KRA_DEPLOY_MYSQL" = 'true' ]; then
test -n "$KRA_MYSQL_ROOT_PASSWORD" || { echo 'MySQL root password credential is empty' >&2; exit 1; }
printf '%s' "$KRA_MYSQL_ROOT_PASSWORD" > "$TMP_DIR/mysql-root-password"
kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-runtime" \\
--from-file=mysql-root-password="$TMP_DIR/mysql-root-password" \\
--dry-run=client -o yaml | kubectl apply -f -
fi
test -n "$KRA_JWT_SIGNING_KEY" || { echo 'JWT signing key credential is empty' >&2; exit 1; }
if [ "$(printf '%s' "$KRA_JWT_SIGNING_KEY" | wc -l)" -ne 0 ]; then
echo 'JWT signing key credential must be a single line' >&2
exit 1
fi
case "$KRA_JWT_SIGNING_KEY" in
*"'"*) echo "JWT signing key credential must not contain a single quote" >&2; exit 1 ;;
esac
CONFIG_TEMPLATE="$TMP_DIR/config.yaml"
envsubst '$APP_NAME $KRA_ENV $KRA_JWT_SIGNING_KEY' < deploy/k8s/config.example.yaml > "$CONFIG_TEMPLATE"
kubectl -n "$KRA_NAMESPACE" create secret generic "${APP_NAME}-config-template" \\
--from-file=config.yaml="$CONFIG_TEMPLATE" \\
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$KRA_NAMESPACE" delete configmap "${APP_NAME}-config-template" --ignore-not-found
export KRA_APP="$APP_NAME"
export KRA_BACKEND_IMAGE="$REGISTRY/$IMAGE_REPO-backend:$IMAGE_TAG"
export KRA_FRONTEND_IMAGE="$REGISTRY/$IMAGE_REPO-frontend:$IMAGE_TAG"
case "$KRA_DEPLOY_MYSQL" in
true) export KRA_MYSQL_REPLICAS=1; export KRA_MYSQL_WAIT_IMAGE="$KRA_MYSQL_IMAGE" ;;
false) export KRA_MYSQL_REPLICAS=0; export KRA_MYSQL_WAIT_IMAGE="$KRA_BACKEND_IMAGE" ;;
*) echo 'KRA_DEPLOY_MYSQL must be true or false' >&2; exit 1 ;;
esac
case "$KRA_DEPLOY_REDIS" in
true) export KRA_REDIS_REPLICAS=1 ;;
false) export KRA_REDIS_REPLICAS=0 ;;
*) echo 'KRA_DEPLOY_REDIS must be true or false' >&2; exit 1 ;;
esac
RENDERED_MANIFEST="$TMP_DIR/manifest.yaml"
envsubst "$K8S_RENDER_VARS" < deploy/k8s/kra.yaml > "$RENDERED_MANIFEST"
# Groovy 三单引号字符串内反斜杠仍是转义前缀:
# 本行正则里的反斜杠序列必须双写,shell 才能收到 grep -E 需要的单反斜杠模式。
if grep -nE '\\$\\{[A-Z_][A-Z0-9_]*\\}' "$RENDERED_MANIFEST"; then
echo 'Rendered Kubernetes manifest contains unresolved template variables' >&2
exit 1
fi
MYSQL_SERVICE_IP=$(kubectl -n "$KRA_NAMESPACE" get service "${APP_NAME}-mysql" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
if [ -n "$MYSQL_SERVICE_IP" ] && [ "$MYSQL_SERVICE_IP" != 'None' ]; then
echo "${APP_NAME}-mysql already exists as a ClusterIP Service; delete it during a maintenance window before switching to headless mode" >&2
exit 1
fi
kubectl apply --dry-run=server -f "$RENDERED_MANIFEST"
kubectl apply -f "$RENDERED_MANIFEST"
if [ "$KRA_DEPLOY_MYSQL" = 'true' ]; then
RUNTIME_SECRET_VERSION=$(kubectl -n "$KRA_NAMESPACE" get secret "${APP_NAME}-runtime" -o jsonpath='{.metadata.resourceVersion}')
PATCH_FILE="$TMP_DIR/mysql-patch.json"
printf '{"spec":{"template":{"metadata":{"annotations":{"kra.dev/runtime-secret-version":"%s"}}}}}\n' \\
"$RUNTIME_SECRET_VERSION" > "$PATCH_FILE"
kubectl -n "$KRA_NAMESPACE" patch statefulset "${APP_NAME}-mysql" --type=merge \\
--patch-file="$PATCH_FILE"
kubectl -n "$KRA_NAMESPACE" rollout status statefulset/${APP_NAME}-mysql --timeout=10m
fi
if [ "$KRA_DEPLOY_REDIS" = 'true' ]; then
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-redis --timeout=10m
fi
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-backend --timeout=10m
kubectl -n "$KRA_NAMESPACE" rollout status deployment/${APP_NAME}-frontend --timeout=10m
'''
}
sshagent(credentials: ['deploy-server-ssh-key']) {
sh '''
ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} "bash /root/deploy.sh ${DEPLOY_IMAGE_TAG}"
'''
}
}
}
}
post {
success {
echo "Deployed ${BACKEND_IMAGE}:${DEPLOY_IMAGE_TAG} / ${FRONTEND_IMAGE}:${DEPLOY_IMAGE_TAG}"
}
}
}
+149
View File
@@ -0,0 +1,149 @@
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
}
parameters {
choice(
name: 'DEPLOY_MODE',
choices: ['BUILD_AND_DEPLOY', 'DEPLOY_ONLY'],
description: 'BUILD_AND_DEPLOY builds and deploys a new image; DEPLOY_ONLY deploys an existing Harbor image tag'
)
string(
name: 'IMAGE_TAG',
defaultValue: '',
description: 'Image tag to deploy. Required for DEPLOY_ONLY; empty BUILD_AND_DEPLOY uses current commit'
)
}
environment {
APP_NAME = 'big-party-rpc-script'
REGISTRY_HOST = '192.168.2.160:8088'
REGISTRY_NAMESPACE = 'zhoubao'
IMAGE_REPOSITORY = "${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${APP_NAME}"
DEPLOY_HOST = 'root@192.168.2.226'
DEPLOY_DIR = "/opt/${APP_NAME}"
GITLAB_PROXY_HOST = '192.168.2.160'
DOCKER_BUILDKIT = '1'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Prepare') {
steps {
script {
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
env.VERSION = sh(script: 'git describe --tags --always --dirty', returnStdout: true).trim()
if (params.DEPLOY_MODE == 'DEPLOY_ONLY') {
if (!params.IMAGE_TAG?.trim()) {
error('IMAGE_TAG is required when DEPLOY_MODE=DEPLOY_ONLY')
}
env.DEPLOY_IMAGE_TAG = params.IMAGE_TAG.trim()
} else {
env.DEPLOY_IMAGE_TAG = params.IMAGE_TAG?.trim() ? params.IMAGE_TAG.trim() : env.GIT_COMMIT_SHORT
}
echo "Deploy mode: ${params.DEPLOY_MODE}"
echo "Image: ${env.IMAGE_REPOSITORY}:${env.DEPLOY_IMAGE_TAG}"
}
}
}
stage('Test') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
withCredentials([sshUserPrivateKey(credentialsId: 'gitlab-go-deps-ssh-key', keyFileVariable: 'GIT_SSH_KEY')]) {
sh '''
git config --global url."git@gitlab.fuhua91.com:".insteadOf "https://gitlab.fuhua91.com/"
git config --global url."git@gitlab.fuhua91.com:".insteadOf "http://gitlab.fuhua91.com/"
export GIT_SSH_COMMAND="ssh -i ${GIT_SSH_KEY} -o StrictHostKeyChecking=no"
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOPRIVATE=gitlab.fuhua91.com
go env -w GOINSECURE=gitlab.fuhua91.com
go test ./...
'''
}
}
}
stage('Build Image') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
withCredentials([sshUserPrivateKey(credentialsId: 'gitlab-go-deps-ssh-key', keyFileVariable: 'GIT_SSH_KEY')]) {
sh '''
docker build \
--file deploy/Dockerfile \
--add-host gitlab.fuhua91.com:${GITLAB_PROXY_HOST} \
--secret id=git_ssh_key,src=${GIT_SSH_KEY} \
--build-arg VERSION=${VERSION} \
--tag ${IMAGE_REPOSITORY}:${DEPLOY_IMAGE_TAG} \
--tag ${IMAGE_REPOSITORY}:latest \
.
'''
}
}
}
stage('Push Image') {
when {
expression { params.DEPLOY_MODE == 'BUILD_AND_DEPLOY' }
}
steps {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASS')]) {
sh '''
echo "${REGISTRY_PASS}" | docker login "${REGISTRY_HOST}" -u "${REGISTRY_USER}" --password-stdin
docker push ${IMAGE_REPOSITORY}:${DEPLOY_IMAGE_TAG}
docker push ${IMAGE_REPOSITORY}:latest
'''
}
}
}
stage('Deploy') {
steps {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASS')]) {
sshagent(credentials: ['deploy-server-ssh-key']) {
sh '''
ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} "mkdir -p ${DEPLOY_DIR}/deploy ${DEPLOY_DIR}/configs ${DEPLOY_DIR}/logs"
scp -O -o StrictHostKeyChecking=no deploy/docker-compose.yaml ${DEPLOY_HOST}:${DEPLOY_DIR}/deploy/docker-compose.yaml
scp -O -o StrictHostKeyChecking=no configs/config.yaml ${DEPLOY_HOST}:${DEPLOY_DIR}/configs/config.yaml
printf '%s' "${REGISTRY_PASS}" | ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} "docker login '${REGISTRY_HOST}' -u '${REGISTRY_USER}' --password-stdin"
ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} "
cd ${DEPLOY_DIR}/deploy &&
printf '%s\\n' \
'IMAGE_REPOSITORY=${IMAGE_REPOSITORY}' \
'IMAGE_TAG=${DEPLOY_IMAGE_TAG}' \
'HTTP_PORT=20200' \
'GRPC_PORT=20201' \
'CONFIG_FILE=../configs/config.yaml' \
'LOG_DIR=../logs' > .env &&
docker-compose pull &&
docker-compose up -d &&
docker image prune -f
"
'''
}
}
}
}
}
post {
success {
echo "Deployed ${IMAGE_REPOSITORY}:${DEPLOY_IMAGE_TAG}"
}
}
}
+115
View File
@@ -0,0 +1,115 @@
# live.fuhua91.com 宝塔站点配置
# 路径规划:
# / -> live 用户端前端(静态目录,或替换为反代)
# /admin/ -> 管理后台(frontend 容器 9501,nginx 去前缀转发)
# /api -> 后端 API/WebSocket(backend 容器 9502)
# 注意: 本文件顶部的 map 位于 http 上下文(宝塔 include 位置),合法。
map $http_upgrade $live_connection_upgrade {
default upgrade;
'' close;
}
server
{
listen 80;
listen 443 ssl http2 ;
server_name live.fuhua91.com;
root /www/wwwroot/live.fuhua91.com;
index index.html;
#CERT-APPLY-CHECK--START
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
include /www/server/panel/vhost/nginx/well-known/live.fuhua91.com.conf;
#CERT-APPLY-CHECK--END
include /www/server/panel/vhost/nginx/extension/live.fuhua91.com/*.conf;
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
#error_page 404/404.html;
ssl_certificate /www/server/panel/vhost/cert/live.fuhua91.com/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/live.fuhua91.com/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END
# 大文件上传(视频分片走 /api)
client_max_body_size 1024m;
# 一键申请SSL证书验证目录: 直接读本地磁盘,不进容器
location ^~ /.well-known/ {
root /www/wwwroot/live.fuhua91.com;
allow all;
}
# 禁止访问的敏感文件
location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$
{
return 404;
}
# 禁止访问的敏感目录
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
return 404;
}
# 后端 API / WebSocket(容器内后端无 /api 前缀,proxy_pass 尾斜杠自动去掉)
location ^~ /api {
proxy_pass http://127.0.0.1:9502;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $live_connection_upgrade;
proxy_set_header Host $http_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_read_timeout 600s;
proxy_send_timeout 600s;
}
# 管理后台(frontend 容器;rewrite 去掉 /admin 前缀,前端已按 /admin/ base 构建)
location = /admin {
return 301 /admin/;
}
location ^~ /admin/ {
rewrite ^/admin/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9501;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
}
# live 用户端前端(根路径)
# 方式一: 静态文件放 /www/wwwroot/live.fuhua91.com/live-web 后使用当前配置
# 方式二: 若 live 前端也是 docker 容器,把下面两行换成:
# proxy_pass http://127.0.0.1:<live前端端口>;
# proxy_set_header Host $http_host; 以及上面 /admin/ 块中的其余 proxy_set_header
location / {
root /www/wwwroot/live.fuhua91.com/live-web;
try_files $uri $uri/ /index.html;
}
#禁止在证书验证目录放入敏感文件
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
return 403;
}
#REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
include /www/server/panel/vhost/rewrite/live.fuhua91.com.conf;
#REWRITE-END
access_log /www/wwwlogs/live.fuhua91.com.log;
error_log /www/wwwlogs/live.fuhua91.com.error.log;
}
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# ============================================================
# live.fuhua91.com 一键部署脚本
#
# 用法:
# ./deploy.sh 拉取 latest 最新镜像并重启
# ./deploy.sh v1.0.0 部署指定版本
#
# 建议: 放到 /root/deploy.sh,执行 chmod 700 /root/deploy.sh
# 依赖: 服务器已安装 docker,且能访问 harbor.fuhua91.com
# ============================================================
set -euo pipefail
# ---------- 可按需调整的参数 ----------
REGISTRY="harbor.fuhua91.com"
PROJECT="live"
# Harbor 机器人账号(也可通过环境变量 HARBOR_USER / HARBOR_PASSWORD 覆盖)
HARBOR_USER_DEFAULT='robot$live+live'
HARBOR_PASSWORD_DEFAULT='R4p6ieacIogXE779eL1HXhCpA4bx4SA7'
BASE_DIR="/www/wwwroot/live.fuhua91.com"
CONFIG_DIR="$BASE_DIR/config" # 后端配置目录 -> 容器内 /data/conf
LOG_DIR="$BASE_DIR/log" # 后端日志目录 -> 容器内 /app/logs
UPLOADS_DIR="$BASE_DIR/uploads" # 上传文件目录 -> 容器内 /app/uploads
FRONTEND_PORT=9501 # 前端 nginx 容器
BACKEND_PORT=9502 # 后端 API/WebSocket 容器
NETWORK="live-net"
BACKEND_NAME="live-backend"
FRONTEND_NAME="live-frontend"
# --------------------------------------
TAG="${1:-latest}"
BACKEND_IMAGE="$REGISTRY/$PROJECT/backend:$TAG"
FRONTEND_IMAGE="$REGISTRY/$PROJECT/frontend:$TAG"
echo "==> 部署版本: $TAG"
# 1. 准备挂载目录
mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$UPLOADS_DIR"
# 2. 首次运行检查配置文件
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
echo "错误: 缺少 $CONFIG_DIR/config.yaml"
echo "请先将项目 configs/config.yaml 复制到该路径,"
echo "并修改其中的 MySQL / Redis 等连接信息后重新执行本脚本。"
exit 1
fi
# 3. 登录 Harbor(优先使用环境变量 HARBOR_USER / HARBOR_PASSWORD)
HARBOR_USER="${HARBOR_USER:-$HARBOR_USER_DEFAULT}"
HARBOR_PASSWORD="${HARBOR_PASSWORD:-$HARBOR_PASSWORD_DEFAULT}"
echo "==> 登录 $REGISTRY"
echo "$HARBOR_PASSWORD" | docker login "$REGISTRY" --username "$HARBOR_USER" --password-stdin >/dev/null
# 4. 拉取最新镜像
echo "==> 拉取镜像"
docker pull "$BACKEND_IMAGE"
docker pull "$FRONTEND_IMAGE"
# 5. 重建容器
echo "==> 重启容器"
docker network create "$NETWORK" >/dev/null 2>&1 || true
docker rm -f "$BACKEND_NAME" "$FRONTEND_NAME" 2>/dev/null || true
# 容器内进程以 uid 1000 (kra) 运行,日志/上传目录需要写权限
chown -R 1000:1000 "$LOG_DIR" "$UPLOADS_DIR" 2>/dev/null || \
chmod -R 777 "$LOG_DIR" "$UPLOADS_DIR"
# 后端: 配置 /data/conf,日志 /app/logs,上传 /app/uploads
docker run -d --name "$BACKEND_NAME" \
--restart always \
--network "$NETWORK" \
-p "$BACKEND_PORT:8000" \
-v "$CONFIG_DIR:/data/conf" \
-v "$LOG_DIR:/app/logs" \
-v "$UPLOADS_DIR:/app/uploads" \
"$BACKEND_IMAGE"
# 前端: nginx 静态站点,容器内已配置 /api -> live-backend:8000
docker run -d --name "$FRONTEND_NAME" \
--restart always \
--network "$NETWORK" \
-p "$FRONTEND_PORT:80" \
"$FRONTEND_IMAGE"
docker logout "$REGISTRY" >/dev/null 2>&1 || true
echo "==> 部署完成"
docker ps --filter "name=$BACKEND_NAME" --filter "name=$FRONTEND_NAME"
echo ""
echo "前端容器: http://<服务器IP>:$FRONTEND_PORT"
echo "后端容器: http://<服务器IP>:$BACKEND_PORT"
echo "提示: 网站正常访问走宝塔 nginx 反代(80/443),9501/9502 为直连调试端口,记得放行防火墙。"