pipeline { agent any options { timestamps() disableConcurrentBuilds() } environment { // ===== Registry ===== REGISTRY_HOST = "registry.pjt.kr" IMAGE_REPO = "alist/api" DOCKER_CRED = "registry-pjt" // ===== api ssh ===== SSH_HOST = "121.160.234.222" SSH_PORT = "2001" SSH_USER = "bigfuntnp" SSH_COMPOSE_DIR = "/srv/project/alist/compose" SSH_COMPOSE_FILE = "api-compose.pjt.yml" // ===== jenkins workspace path ===== WORK_COMPOSE_DIR = "deploy/pjt/compose" WORK_COMPOSE_FILE = "api-compose.pjt.yml" } stages { stage('1) Git check out') { steps { echo "[1/1] Git check out" checkout scm } } stage('2) Build (Gradle)') { steps { sh ''' set -e echo "[1/1] Gradle build" chmod +x ./gradlew ./gradlew clean build -x test ''' } } stage('3) ImageTag create') { steps { script { echo "[1/1] ImageTag create" env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() echo "SHORT_SHA = ${env.SHORT_SHA}" env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" echo "IMAGE_TAG = ${env.IMAGE_TAG}" } } } stage('4) Docker Build') { steps { sh ''' set -e echo "[1/1] docker build" docker build -t ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} . ''' } } stage('5) Docker registry Login & Push') { steps { withCredentials([usernamePassword( credentialsId: DOCKER_CRED, usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { sh ''' set -e echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} \ -u "$DOCKER_USER" --password-stdin echo "[2/2] docker image push" docker push ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} docker logout ${REGISTRY_HOST} ''' } } } stage('6) Jenkins cleanup') { steps { sh ''' set -e echo "[1/4] Remove pushed image from Jenkins" docker rmi ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} 2>/dev/null || true echo "[2/4] Remove dangling images" docker image prune -f echo "[3/4] Remove old build cache (safe-ish)" # 빌드 캐시만 정리: 다른 컨테이너/볼륨에는 영향 적음 docker builder prune -f || true echo "[4/4] Skip docker system prune --volumes (too aggressive on shared nodes)" ''' } } stage('7) Copy compose') { steps { sshagent(['ssh-pjt']) { sh ''' set -e echo "[1/2] ssh login" SRC="${WORK_COMPOSE_DIR}/${WORK_COMPOSE_FILE}" DST="${SSH_USER}@${SSH_HOST}:${SSH_COMPOSE_DIR}/${SSH_COMPOSE_FILE}" echo "[2/2] Copy compose $SRC -> $DST" scp -P ${SSH_PORT} -o StrictHostKeyChecking=no "$SRC" "$DST" ''' } } } stage('8) Remote docker registry login') { steps { sshagent(['ssh-pjt']) { withCredentials([usernamePassword( credentialsId: DOCKER_CRED, usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { sh ''' set -e echo "[1/1] docker registry login" ssh -p ${SSH_PORT} \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ 'set -e REGISTRY_HOST='"${REGISTRY_HOST}"' DOCKER_USER='"${DOCKER_USER}"' DOCKER_PASS='"${DOCKER_PASS}"' echo "$DOCKER_PASS" | docker login "$REGISTRY_HOST" -u "$DOCKER_USER" --password-stdin ' ''' } } } } stage('9) Remote docker pull') { steps { sshagent(['ssh-pjt']) { sh ''' set -e ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ 'set -e cd '"${SSH_COMPOSE_DIR}"' export IMAGE_TAG='"${IMAGE_TAG}"' echo "[1/1] docker pull" docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' pull ' ''' } } } stage('10) Remote docker down & up') { steps { sshagent(['ssh-pjt']) { sh ''' set -e ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ 'set -e cd '"${SSH_COMPOSE_DIR}"' export IMAGE_TAG='"${IMAGE_TAG}"' echo "[1/3] docker down (remove orphans)" docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f "'"${SSH_COMPOSE_FILE}"'" down --remove-orphans echo "[2/3] docker up" docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' up -d echo "[3/3] docker ps" docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' ps ' ''' } } } stage('11) Remote cleanup') { steps { sshagent(['ssh-pjt']) { sh ''' set -e ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ 'set -e echo "[0] Collect images used by running containers" RUNNING_IMAGES=$(docker ps -q | xargs -r docker inspect --format="{{.Image}}" | sort -u) echo "[1/3] Remove old registry images (except running ones)" docker images '"${REGISTRY_HOST}/${IMAGE_REPO}"' --format "{{.ID}} {{.CreatedAt}}" | \ sort -k2 -r | \ awk "{print \$1}" | \ while read IMAGE_ID; do echo "$RUNNING_IMAGES" | grep -q "$IMAGE_ID" && continue docker rmi -f "$IMAGE_ID" 2>/dev/null || true done echo "[2/3] Remove dangling images (safe)" docker image prune -f echo "[3/3] Remove unused volumes (safe)" docker volume prune -f ' ''' } } } stage('12) Registry cleanup (keep last 5 + protect running)') { steps { sshagent(['ssh-pjt']) { sh ''' set -e ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ 'set -e REGISTRY_URL="https://'${REGISTRY_HOST}'" # 예: https://registry.pjt.kr REPO="'${IMAGE_REPO}'" # 예: alist/api KEEP=5 # (옵션) Basic Auth 쓰면 아래 활성화하고 curl에 -u "$AUTH" 추가 # AUTH="user:pass" echo "[0/5] Ensure jq exists" if ! command -v jq >/dev/null 2>&1; then sudo apt-get update -y sudo apt-get install -y jq fi echo "[1/5] Collect running tags to protect" # 현재 서버에서 실행 중인 컨테이너가 사용하는 해당 레포 이미지 태그만 추출 RUNNING_TAGS=$(docker ps --format "{{.Image}}" | \ grep -E "^'${REGISTRY_HOST}'/'${IMAGE_REPO}':" | \ awk -F: "{print \\$2}" | sort -u || true) if [ -n "$RUNNING_TAGS" ]; then echo "Running tags (protected):" echo "$RUNNING_TAGS" | sed "s/^/ - /" else echo "No running tags found for ${REGISTRY_HOST}/${IMAGE_REPO} (ok)" fi echo "[2/5] Fetch tags from registry: $REPO" TAGS=$(curl -fsS "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) # Basic Auth 쓰면 위 curl을 아래로 바꿔: # TAGS=$(curl -fsS -u "$AUTH" "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) if [ -z "$TAGS" ]; then echo "No tags found in registry. Skip." exit 0 fi echo "[3/5] Sort tags desc (tag must be sortable for 'latest' semantics)" SORTED=$(echo "$TAGS" | sort -r) KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) echo "Keep tags (latest $KEEP):" echo "$KEEP_TAGS" | sed "s/^/ - /" echo "[4/5] Delete old tags except protected running tags" for TAG in $DEL_CANDIDATES; do # 실행 중 태그는 무조건 스킵 if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then echo "Skip running tag: $REPO:$TAG" continue fi echo "Deleting $REPO:$TAG" DIGEST=$(curl -fsSI \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") # Basic Auth 쓰면 위 curl을 아래로 바꿔: # DIGEST=$(curl -fsSI -u "$AUTH" \ # -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ # "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ # | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") if [ -z "$DIGEST" ]; then echo " digest not found. skip $TAG" continue fi curl -fsS -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true # Basic Auth 쓰면: # curl -fsS -u "$AUTH" -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true done echo "[5/5] Garbage collect (reclaim disk space)" # push가 진행 중이면 위험할 수 있으니, 배포 성공 후 마지막 단계에서만 실행 추천 docker exec registry registry garbage-collect /etc/docker/registry/config.yml echo "Registry cleanup done." ' ''' } } } } }