docker driver only supports a single instance. Creating another fails with 'additional instances of driver docker cannot be created'. Just use the default builder that ships with the docker daemon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
326 lines
15 KiB
Groovy
326 lines
15 KiB
Groovy
pipeline {
|
|
agent { label '\u6784\u5efa\u673a1' }
|
|
|
|
options {
|
|
timestamps()
|
|
disableConcurrentBuilds()
|
|
skipDefaultCheckout(true)
|
|
}
|
|
|
|
environment {
|
|
REGISTRY_URL = 'reg.nxsir.cn'
|
|
API_IMAGE_NAME = 'live_recorder/app-api'
|
|
WEB_IMAGE_NAME = 'live_recorder/app-web'
|
|
IMAGE_TAG = "${env.BUILD_ID}"
|
|
DOCKER_CREDS = 'harbor_key'
|
|
TARGET_PLATFORMS = 'linux/amd64,linux/arm64'
|
|
BUILDER_NAME = 'liverecorder-buildx'
|
|
WEB_NODE_IMAGE = 'docker.m.daocloud.io/library/node:22-alpine'
|
|
WEB_NGINX_IMAGE = 'docker.m.daocloud.io/library/nginx:1.27-alpine'
|
|
HTTP_PROXY_URL = 'http://192.168.5.200:7890'
|
|
// Tsinghua/daocloud/mcr hosts are reachable directly from the builder — routing them
|
|
// through the 7890 proxy caused intermittent '502 Bad Gateway' during apt-get install.
|
|
NO_PROXY_HOSTS = '127.0.0.1,localhost,reg.nxsir.cn,gitea.nxsir.cn,mirrors.tuna.tsinghua.edu.cn,.tsinghua.edu.cn,mcr.microsoft.com,docker.m.daocloud.io'
|
|
|
|
GIT_REPO_URL = 'https://gitea.nxsir.cn/nanxun/live_recorder.git'
|
|
GIT_BRANCH = 'main'
|
|
GIT_CREDS = ''
|
|
|
|
API_IMAGE_TAGGED = "${REGISTRY_URL}/${API_IMAGE_NAME}:${IMAGE_TAG}"
|
|
API_IMAGE_LATEST = "${REGISTRY_URL}/${API_IMAGE_NAME}:latest"
|
|
WEB_IMAGE_TAGGED = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:${IMAGE_TAG}"
|
|
WEB_IMAGE_LATEST = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:latest"
|
|
API_CACHE_IMAGE = "${REGISTRY_URL}/${API_IMAGE_NAME}:buildcache"
|
|
WEB_CACHE_IMAGE = "${REGISTRY_URL}/${WEB_IMAGE_NAME}:buildcache"
|
|
}
|
|
|
|
stages {
|
|
stage('Checkout') {
|
|
steps {
|
|
script {
|
|
def userRemoteConfig = [url: env.GIT_REPO_URL]
|
|
if (env.GIT_CREDS?.trim()) {
|
|
userRemoteConfig.credentialsId = env.GIT_CREDS.trim()
|
|
}
|
|
|
|
checkout([
|
|
$class: 'GitSCM',
|
|
branches: [[name: "*/${env.GIT_BRANCH}"]],
|
|
userRemoteConfigs: [userRemoteConfig]
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Prepare Buildx') {
|
|
steps {
|
|
sh """
|
|
set -e
|
|
sudo docker buildx version
|
|
|
|
# Register ARM64 binfmt. Docker Hub is unreachable from this builder,
|
|
# so try daocloud mirror first; fall back to apt-installed qemu-user-static.
|
|
if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ] && [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64-static ]; then
|
|
echo 'Registering ARM64 emulation (binfmt not present)...'
|
|
if ! sudo docker run --privileged --rm docker.m.daocloud.io/tonistiigi/binfmt --install arm64 2>/dev/null; then
|
|
echo 'daocloud mirror failed, falling back to qemu-user-static via apt...'
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq qemu-user-static
|
|
fi
|
|
else
|
|
echo 'ARM64 binfmt already registered, skipping.'
|
|
fi
|
|
|
|
# Verify emulation works before proceeding
|
|
sudo docker run --rm --platform linux/arm64 docker.m.daocloud.io/library/alpine:latest uname -m
|
|
|
|
# Use the default docker builder (docker driver) — not a separate
|
|
# docker-container instance — so we don't need to pull moby/buildkit
|
|
# from unreachable Docker Hub.
|
|
sudo docker buildx use default 2>/dev/null || true
|
|
sudo docker buildx inspect --bootstrap >/dev/null
|
|
"""
|
|
}
|
|
}
|
|
|
|
stage('Build And Push API Image') {
|
|
steps {
|
|
withCredentials([
|
|
usernamePassword(
|
|
credentialsId: "${DOCKER_CREDS}",
|
|
usernameVariable: 'DOCKER_USERNAME',
|
|
passwordVariable: 'DOCKER_PASSWORD'
|
|
)
|
|
]) {
|
|
sh """
|
|
set -e
|
|
|
|
run_with_heartbeat() {
|
|
log_file="\$1"
|
|
build_label="\$2"
|
|
shift 2
|
|
rm -f "\$log_file"
|
|
: > "\$log_file"
|
|
"\$@" >"\$log_file" 2>&1 &
|
|
cmd_pid=\$!
|
|
cmd_status=0
|
|
last_size=0
|
|
while kill -0 "\$cmd_pid" >/dev/null 2>&1; do
|
|
echo "[heartbeat] \${build_label} still running at \$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
if [ -f "\$log_file" ]; then
|
|
current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0)
|
|
if [ "\$current_size" -gt "\$last_size" ]; then
|
|
start_byte=\$((last_size + 1))
|
|
tail -c +"\$start_byte" "\$log_file" || true
|
|
last_size=\$current_size
|
|
fi
|
|
fi
|
|
sleep 20
|
|
done
|
|
wait "\$cmd_pid" || cmd_status=\$?
|
|
if [ -f "\$log_file" ]; then
|
|
current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0)
|
|
if [ "\$current_size" -gt "\$last_size" ]; then
|
|
start_byte=\$((last_size + 1))
|
|
tail -c +"\$start_byte" "\$log_file" || true
|
|
fi
|
|
fi
|
|
if [ "\$cmd_status" -ne 0 ]; then
|
|
echo "[heartbeat] \${build_label} failed with exit code \$cmd_status"
|
|
fi
|
|
return "\$cmd_status"
|
|
}
|
|
|
|
# ── Build each platform separately, push with docker CLI,
|
|
# then assemble a multi-arch manifest.
|
|
# buildx --push does not forward auth reliably on this
|
|
# builder; per-platform --load + docker push + manifest
|
|
# bypasses that entirely.
|
|
for PLATFORM in linux/amd64 linux/arm64; do
|
|
ARCH_TAG="\${PLATFORM##*/}" # amd64 / arm64
|
|
PLAT_REF="${API_IMAGE_TAGGED}-\${ARCH_TAG}"
|
|
PLAT_LATEST="${API_IMAGE_LATEST}-\${ARCH_TAG}"
|
|
|
|
echo ''
|
|
echo "=== Building API for \$PLATFORM -> \$PLAT_REF ==="
|
|
run_with_heartbeat /tmp/live-recorder-api-\${ARCH_TAG}.log "API \${ARCH_TAG} build" \
|
|
sudo docker buildx build \
|
|
--platform "\$PLATFORM" \
|
|
--network host \
|
|
--progress=plain \
|
|
--provenance=false \
|
|
--no-cache \
|
|
--build-arg HTTP_PROXY=${HTTP_PROXY_URL} \
|
|
--build-arg HTTPS_PROXY=${HTTP_PROXY_URL} \
|
|
--build-arg NO_PROXY=${NO_PROXY_HOSTS} \
|
|
--build-arg http_proxy=${HTTP_PROXY_URL} \
|
|
--build-arg https_proxy=${HTTP_PROXY_URL} \
|
|
--build-arg no_proxy=${NO_PROXY_HOSTS} \
|
|
-f src/LiveRecorder.WebApi/Dockerfile \
|
|
-t "\$PLAT_REF" \
|
|
-t "\$PLAT_LATEST" \
|
|
--load \
|
|
.
|
|
|
|
echo "=== Pushing \$PLAT_REF ==="
|
|
# Write Harbor auth directly to config.json (bypass docker
|
|
# login entirely) — then push with fresh baked-in credentials.
|
|
AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0)
|
|
printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null
|
|
sudo docker push "\$PLAT_REF" 2>&1 | tail -5
|
|
sudo docker push "\$PLAT_LATEST" 2>&1 | tail -5
|
|
|
|
# Free disk space before the next platform build
|
|
sudo docker rmi "\$PLAT_REF" "\$PLAT_LATEST" 2>/dev/null || true
|
|
done
|
|
|
|
echo ''
|
|
echo "=== Creating multi-arch manifest: ${API_IMAGE_TAGGED} ==="
|
|
AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null
|
|
sudo docker manifest create "${API_IMAGE_TAGGED}" \
|
|
"${API_IMAGE_TAGGED}-amd64" \
|
|
"${API_IMAGE_TAGGED}-arm64"
|
|
sudo docker manifest create --amend "${API_IMAGE_LATEST}" \
|
|
"${API_IMAGE_LATEST}-amd64" \
|
|
"${API_IMAGE_LATEST}-arm64"
|
|
|
|
echo "=== Pushing manifests ==="
|
|
sudo docker manifest push "${API_IMAGE_TAGGED}" 2>&1 | tail -3
|
|
sudo docker manifest push "${API_IMAGE_LATEST}" 2>&1 | tail -3
|
|
|
|
# (Optionally clean up per-platform tags from registry, but harmless to leave them)
|
|
echo ''
|
|
echo "API multi-arch images pushed successfully:"
|
|
echo " ${API_IMAGE_TAGGED}"
|
|
echo " ${API_IMAGE_LATEST}"
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Build And Push Web Image') {
|
|
steps {
|
|
withCredentials([
|
|
usernamePassword(
|
|
credentialsId: "${DOCKER_CREDS}",
|
|
usernameVariable: 'DOCKER_USERNAME',
|
|
passwordVariable: 'DOCKER_PASSWORD'
|
|
)
|
|
]) {
|
|
sh """
|
|
set -e
|
|
|
|
run_with_heartbeat() {
|
|
log_file="\$1"
|
|
build_label="\$2"
|
|
shift 2
|
|
rm -f "\$log_file"
|
|
: > "\$log_file"
|
|
"\$@" >"\$log_file" 2>&1 &
|
|
cmd_pid=\$!
|
|
cmd_status=0
|
|
last_size=0
|
|
while kill -0 "\$cmd_pid" >/dev/null 2>&1; do
|
|
echo "[heartbeat] \${build_label} still running at \$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
if [ -f "\$log_file" ]; then
|
|
current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0)
|
|
if [ "\$current_size" -gt "\$last_size" ]; then
|
|
start_byte=\$((last_size + 1))
|
|
tail -c +"\$start_byte" "\$log_file" || true
|
|
last_size=\$current_size
|
|
fi
|
|
fi
|
|
sleep 20
|
|
done
|
|
wait "\$cmd_pid" || cmd_status=\$?
|
|
if [ -f "\$log_file" ]; then
|
|
current_size=\$(stat -c%s "\$log_file" 2>/dev/null || echo 0)
|
|
if [ "\$current_size" -gt "\$last_size" ]; then
|
|
start_byte=\$((last_size + 1))
|
|
tail -c +"\$start_byte" "\$log_file" || true
|
|
fi
|
|
fi
|
|
if [ "\$cmd_status" -ne 0 ]; then
|
|
echo "[heartbeat] \${build_label} failed with exit code \$cmd_status"
|
|
fi
|
|
return "\$cmd_status"
|
|
}
|
|
|
|
for PLATFORM in linux/amd64 linux/arm64; do
|
|
ARCH_TAG="\${PLATFORM##*/}"
|
|
PLAT_REF="${WEB_IMAGE_TAGGED}-\${ARCH_TAG}"
|
|
PLAT_LATEST="${WEB_IMAGE_LATEST}-\${ARCH_TAG}"
|
|
|
|
echo ''
|
|
echo "=== Building Web for \$PLATFORM -> \$PLAT_REF ==="
|
|
run_with_heartbeat /tmp/live-recorder-web-\${ARCH_TAG}.log "Web \${ARCH_TAG} build" \
|
|
sudo docker buildx build \
|
|
--platform "\$PLATFORM" \
|
|
--network host \
|
|
--progress=plain \
|
|
--provenance=false \
|
|
--no-cache \
|
|
--build-arg NODE_IMAGE=${WEB_NODE_IMAGE} \
|
|
--build-arg NGINX_IMAGE=${WEB_NGINX_IMAGE} \
|
|
--build-arg HTTP_PROXY=${HTTP_PROXY_URL} \
|
|
--build-arg HTTPS_PROXY=${HTTP_PROXY_URL} \
|
|
--build-arg NO_PROXY=${NO_PROXY_HOSTS} \
|
|
--build-arg http_proxy=${HTTP_PROXY_URL} \
|
|
--build-arg https_proxy=${HTTP_PROXY_URL} \
|
|
--build-arg no_proxy=${NO_PROXY_HOSTS} \
|
|
-f frontend/Dockerfile \
|
|
--build-arg VITE_API_BASE_URL=/api \
|
|
-t "\$PLAT_REF" \
|
|
-t "\$PLAT_LATEST" \
|
|
--load \
|
|
frontend
|
|
|
|
echo "=== Pushing \$PLAT_REF ==="
|
|
AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null
|
|
sudo docker push "\$PLAT_REF" 2>&1 | tail -3
|
|
sudo docker push "\$PLAT_LATEST" 2>&1 | tail -3
|
|
sudo docker rmi "\$PLAT_REF" "\$PLAT_LATEST" 2>/dev/null || true
|
|
done
|
|
|
|
echo ''
|
|
echo "=== Creating multi-arch manifest: ${WEB_IMAGE_TAGGED} ==="
|
|
AUTH_B64=\$(printf '%s:%s' "\$DOCKER_USERNAME" "\$DOCKER_PASSWORD" | base64 -w0) && printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_URL}" "\$AUTH_B64" | sudo tee /root/.docker/config.json >/dev/null
|
|
sudo docker manifest create "${WEB_IMAGE_TAGGED}" \
|
|
"${WEB_IMAGE_TAGGED}-amd64" \
|
|
"${WEB_IMAGE_TAGGED}-arm64"
|
|
sudo docker manifest create --amend "${WEB_IMAGE_LATEST}" \
|
|
"${WEB_IMAGE_LATEST}-amd64" \
|
|
"${WEB_IMAGE_LATEST}-arm64"
|
|
|
|
echo "=== Pushing manifests ==="
|
|
sudo docker manifest push "${WEB_IMAGE_TAGGED}" 2>&1 | tail -3
|
|
sudo docker manifest push "${WEB_IMAGE_LATEST}" 2>&1 | tail -3
|
|
|
|
echo ''
|
|
echo "Web multi-arch images pushed successfully:"
|
|
echo " ${WEB_IMAGE_TAGGED}"
|
|
echo " ${WEB_IMAGE_LATEST}"
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
post {
|
|
success {
|
|
echo "Pipeline completed successfully."
|
|
}
|
|
failure {
|
|
echo "Pipeline failed. Please check the build log."
|
|
}
|
|
always {
|
|
sh """
|
|
set +e
|
|
sudo docker logout ${REGISTRY_URL} >/dev/null 2>&1 || true
|
|
true
|
|
"""
|
|
deleteDir()
|
|
}
|
|
}
|
|
}
|