Jenkins로 CI/CD를 설계할 때는 “빌드를 어디서 하고, 어떤 형태의 결과물을 어디서 어떻게 실행할 것인가”를 먼저 정리하는 것이 좋습니다. 본글에서 실무에서 자주 쓰이는 세 가지 패턴을 중심으로 정리하려고 합니다. 해당 패턴들은 배포 서버 구조, 보안(아웃바운드 규칙 등), 팀 규모에 따라 선택이 달라질 수 있습니다.
이 패턴은 가장 원초적인 방법으로, 가장 쉬운 방법입니다. Jenkins가 원격 서버에 접속해 git pull 과 docker compose 또는 Dockerfile 를 실행하게 하는 방식입니다. 즉, 코드 체크아웃과 컨테이너 빌드·실행을 모두 “배포 대상 서버”에서 처리합니다.
# 해당 디렉토리 이동
cd /home/ubuntu/app
# git pull
git pull origin main
# docker compose 내림. 볼륨은 선택
docker compose -f docker-compose.yml down -v
# docker compose 실행
docker compose -f docker-compose.yml up --build -d
# 기존 도커 이미지 삭제
docker image prune -f
이 패턴은 “단일/소규모 서버 + 간단한 서비스” 환경에서 시작하기 좋습니다. 초기 세팅이 간단하고, 서버 한 대로 사이드 프로젝트 또는 PoC용 환경에 적합합니다.
해당 패턴은 Jenkins가 애플리케이션을 빌드하고 Docker 이미지를 생성한 뒤, 레지스트리(Docker Hub, ECR 등)에 저장(push) 후에 배포 서버에서는 이미지만 pull 해서 실행하는 방식입니다.
패턴 1방식에서 깃 저장소가 아닌 Docker 이미지를 저장하는 곳으로 변경했다고 이해하시면 됩니다.
stage("Build & Push Image"){
steps{
sh ""
docker build -t my-registry/app:${BUILD_NUMBER} .
docker push my-registry/app:${BUILD_NUMBER}
""
}
}
# docker 이미지 가져오기
docker pull my-registry/app:${BUILD_NUMBER}
# 이미지 실행
docker compose up -d
Docker 이미지 라는 표준 아티팩트가 남아 롤백과 추적이 쉽다.Kubernetes 등 다른 오케스트레이션으로 확장하기 좋다.이 패턴은 Jenkins가 바이너리(JAR, dist, tar, 도커 이미지 등)를 직접 빌드한 후, SSH를 통해 배포 서버로 해당 내용을 전송하고, 이를 원격에서 스크립트를 실행하는 방식입니다.
젠킨스 서버가 별도로 있고, 배포 서버에서 아웃바운드가 모두 막혀있는 경우 배포 서버에서는 빌드 자체가 불가능하기 때문에 사용한 적이 있습니다.
여기서는 Spring Boot 를 JAR로 빌드하는 것이 아닌 Docker 이미지 빌드하며
.tar파일을 저장하고 전송하는 것으로 예를 들겠습니다.
Publish Over SSH 또는 ssh/scp 로 빌드 결과물 전송 load → docker compose 로 서비스 재기동 Build JAR)첫 번째 스테이지에서는 로컬(젠킨스 서버)에서 Spring Boot JAR를 빌드합니다.
stage('Build JAR') {
steps {
script {
dir("${env.LOCAL_SPRING_DIR}") {
// Gradle Wrapper 실행 권한 부여
sh "chmod +x gradlew"
// 테스트는 제외하고 bootJar 빌드
sh "./gradlew clean bootJar -x test"
}
}
}
}
Build & Save Image)두 번쨰 스테이지에서는 방금 빌드된 JAR를 기반으로 Docker 이미지를 만들고, 이를 .tar 파일로 저장합니다.
stage('Build & Save Image') {
steps {
script {
// 2-1. Spring Boot 이미지 빌드 (Dual Tags: latest, git-tag)
sh """
docker build --no-cache \
-t $IMG_SPRING \
-t ${imgTagged} \
-f $LOCAL_SPRING_DIR/$DOCKERFILE $LOCAL_SPRING_DIR
"""
// 2-2. 도커 이미지를 tar 파일로 저장
sh "docker save -o $TAR_SPRING $IMG_SPRING ${imgTagged}"
}
}
}
--no-cache옵션은 캐시를 사용하지 않고 항상 깨끗한 빌드를 수행하는 옵션입니다. 이미지 레이어에서 변동된 코드 반영이 안 될 위험을 줄이는 대신, 빌드 시간이 조금 더 길어집니다.
Transfer to Remote)세 번째 스테이지에서는 SSH 키를 이용해 원격 서버에 접속하고, docker save 로 만든 .tar 파일을 전송합니다.
stage('Transfer to Remote') {
steps {
withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: 'SSH_KEY')]) {
script {
// 1. 원격 디렉토리 생성
sh '''
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "mkdir -p $DEPLOY_ROOT"
'''
// 2. 대용량 Tar 파일 전송
sh '''
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no "$TAR_SPRING" "$REMOTE_USER@$REMOTE_HOST:$DEPLOY_ROOT/"
'''
// 3. 전송 후 로컬 캐시 삭제
sh '''
rm "$TAR_SPRING"
'''
}
}
}
}
withCredentials([sshUserPrivateKey(...])SSH_KEY)로 꺼내서 사용합니다.mkdir -p $DEPLOY_ROOTp 옵션을 사용합니다.scp 전송 후 rm.tar 파일을 바로 삭제합니다. 장기 운영 시 불필요한 아티팩트가 쌓이지 않도록 하는 실무적인 처리입니다.Remote Deploy)마지막 스테이지에서는 다시 SSH로 원격 서버에 접속하여, 기존 컨테이너를 내리고 새 이미지를 load한 뒤 docker compose로 재기동합니다.
stage('Remote Deploy') {
steps {
withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: 'SSH_KEY')]) {
script {
sh '''
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "
cd $DEPLOY_ROOT
# 1. 기존 컨테이너 종료
docker compose -f $COMPOSE_FILE down
# 2. 로그 디렉토리 생성 및 권한 설정
mkdir -p logs/fastapi logs/spring
chmod -R 755 logs 2>/dev/null || echo 'Log directory exists'
# 3. 새로운 Spring Boot 이미지 로드
docker load -i $TAR_SPRING
# 4. 서비스 실행 (이미지 기반)
docker compose -f $COMPOSE_FILE up -d --no-build
# 5. tar 파일 삭제
rm $TAR_SPRING
# 6. 미사용 이미지 정리
docker image prune -f
"
'''
}
}
}
}
docker compose -f $COMPOSE_FILE downlogs/fastapi, logs/spring를 미리 만들고 권한을 맞춰줌으로써, 컨테이너 내부에서 호스트 볼륨을 마운트할 때 권한 문제를 최소화합니다.docker load -i $TAR_SPRING.tar 파일에서 이미지를 로드합니다. 이 시점 이후에 docker compose는 새 이미지를 사용할 수 있습니다.docker compose up -d --no-build-no-build 옵션으로 불필요한 빌드를 방지합니다.rm $TAR_SPRING, docker image prune -f로 디스크를 정리하여, 장기적으로 서버 용량 이슈를 예방합니다.| 항목 | git pull + compose (패턴 1) | Docker 이미지(패턴 2) | 빌드 후 SSH 전송(패턴 3) |
|---|---|---|---|
| 빌드 위치 | 배포 서버 | Jenkins (또는 빌드 전용 노드) | Jenkins |
| 배포 아티팩트 | Git 워킹 디렉터리 + 컨테이너 | Docker 이미지 | JAR/ZIP/바이너리 파일 |
| Docker 필요 위치 | 배포 서버 | Jenkins + 배포 서버 | 선택 (필수 아님) |
| 롤백 난이도 | 브랜치/태그 기반 수동 롤백 | 태그된 이미지로 비교적 쉬운 롤백 | 아티팩트 보관/스크립트에 따라 상이 |
| 초기 도입 난이도 | 낮음 | 중간(레지스트리, Docker 구성 필요) | 중간(SSH/키 관리 필요) |
| 확장성(K8s 등) | 낮음 | 높음 | 중간 |
| 적합한 상황 | 소규모 서버, 빠른 PoC | 멀티 서버/쿠버네티스, 표준화된 배포 필요 | 레거시 서버, 단일 JAR/바이너리 배포 환경, 보안 정책 |