1. 🚨 문제 발생: 잘 되던 파이프라인이 멈췄다
DoEatFit 프로젝트는 dev 브랜치에 코드가 푸시되면 Jenkins가 자동으로 빌드, 테스트 후 원격 서버에 배포하는 CI/CD 파이프라인을 잘 사용하고 있었습니다.
포트폴리오 정리를 하다 GitHub Organization의 이름을 [OLD_ORG_NAME]에서 DoEatFit으로 변경했습니다. 아무생각 없이 한 행동으로 Jenkins 파이프라인은 그날부터 멈췄습니다.
기존 파이프라인 아키텍처 (Private Repo 기반)
- Trigger:
PrivateGitHub Repo (dev브랜치)에push- CI (Jenkins):
Jenkinsfile실행
- Checkout
- Build Docker Image (
next build)- Push Docker Image to
GHCR(GitHub Container Registry)- CD (Jenkins):
- SSH로 원격 서버(
[REMOTE_USER]@[REMOTE_IP]) 접속docker pull(from GHCR)docker compose down && docker compose up -d
2. 1차 실패: owner not found
이름 변경 후 첫 파이프라인 실행은 Push Image to GHCR 단계에서 즉시 실패했습니다.
[실패 로그 1]
[Pipeline] { (⬆️ Push Image to GHCR)
+ docker login ghcr.io -u [GHCR_USER] --password-stdin
Login Succeeded
+ docker push ghcr.io/[OLD_ORG_NAME]/doeatfit_front:dev
The push refers to repository [ghcr.io/[OLD_ORG_NAME]/doeatfit_front]
...
denied: not_found: owner not found
[Pipeline] }
ERROR: ❌ 이미지 푸시 실패2-1. 원인 분석
원인은 명확했습니다. docker login은 제 개인 계정(PAT)으로 성공했지만, 이미지를 푸시하려는 대상인 ghcr.io/[OLD_ORG_NAME]/...라는 소유자(Organization)는 더 이상 존재하지 않았습니다.
GitHub 리포지토리 URL은 이름이 변경되어도 리디렉션을 지원하지만, GHCR(Docker Registry) 경로는 리디렉션을 지원하지 않았습니다.
2-2. 해결 과정
Jenkinsfile 내부에서 이전 조직 이름([OLD_ORG_NAME])을 하드코딩한 모든 부분을 새 조직 이름(DoEatFit)으로 수정해야 했습니다.
// Jenkinsfile
pipeline {
agent any
stages {
stage('🚚 Checkout') {
steps {
// 1. Git 리포지토리 주소 변경
// (SSH 주소는 리디렉션이 안 될 수 있으므로 명시적으로 변경)
git credentialsId: '[GIT_SSH_CREDENTIAL_ID]',
url: 'git@github.com:DoEatFit/doeatfit_front.git'
}
}
stage('📦 Build Docker Image') {
steps {
// 2. Docker 이미지 태그 변경
sh 'docker build -t ghcr.io/DoEatFit/doeatfit_front:dev .'
}
}
stage('⬆️ Push Image to GHCR') {
// ...
steps {
// 3. Docker Push 경로 변경
sh 'docker push ghcr.io/DoEatFit/doeatfit_front:dev'
}
}
// ... (Deploy to Remote Server 단계)
}
}
3. 2차 실패: manifest unknown
1차 수정을 마치고 다시 빌드를 실행했습니다. Checkout, Build, Push까지 모두 성공(✅)하는 것을 보고 안심했습니다.
하지만 마지막 🚀 Deploy to Remote Server 단계에서 또다시 실패했습니다.
[실패 로그 2]
[Pipeline] { (🚀 Deploy to Remote Server)
+ docker --context remotedocker pull ghcr.io/DoEatFit/doeatfit_front:dev
Status: Downloaded newer image for ghcr.io/DoEatFit/doeatfit_front:dev <- (Pull 성공??)
...
+ docker --context remotedocker compose up -d
doeatfit-frontend Pulling
doeatfit-frontend Error manifest unknown <- (Compose 실패!)
Error response from daemon: manifest unknown
[Pipeline] }
ERROR: ❌ 배포 실패3-1. 원인 분석
로그가 정말 혼란스러웠습니다. docker pull은 성공했는데, 바로 다음 명령어인 docker compose up이 이미지를 못 찾겠다며 실패했습니다.
한참을 헤매다 원인을 찾았습니다. 문제는 Jenkinsfile이 아니었습니다.
Jenkinsfile의 Deploy 단계는 docker-compose.yaml 파일을 원격 서버로 복사한 뒤 실행합니다. 그런데 이 docker-compose.yaml 파일은 Git 리포지토리에 포함된 것이 아니라, 보안을 위해 Jenkins의 Credentials (Secret file) 기능을 통해 주입되고 있었습니다.
// Jenkinsfile (일부)
stage('🔑 Add Config Files') {
steps {
// ...
withCredentials([file(credentialsId: 'doeatfit-compose-file', variable: 'DOCKER_COMPOSE_FILE')]) {
// 이 파일이 문제였습니다.
sh 'cp -p $DOCKER_COMPOSE_FILE docker-compose.yaml'
}
}
}Jenkins Credential에 저장된 docker-compose.yaml 파일의 내용이 수정되지 않았던 것입니다.
-
Jenkinsfile의pull명령어는 올바른 새 경로(ghcr.io/DoEatFit/...)를 사용했습니다. (로그의Pull 성공부분) -
하지만
docker compose up명령어는 Jenkins Credential에 저장된 옛날docker-compose.yaml파일(ghcr.io/[OLD_ORG_NAME]/...)을 읽으려 시도했습니다. -
compose가pulling을 시도할 때,[OLD_ORG_NAME]이 존재하지 않으므로manifest unknown오류가 발생한 것이었습니다.
3-2. 해결 과정
Jenkins 대시보드에서 doeatfit-compose-file Credential(Secret file 타입)을 찾아, 그 내용을 새 조직 이름으로 수정하고 다시 업로드했습니다.
# [수정 전] Jenkins Credential (doeatfit-compose-file)
services:
doeatfit-frontend:
image: ghcr.io/[OLD_ORG_NAME]/doeatfit_front:dev
# ...
# [수정 후] Jenkins Credential (doeatfit-compose-file)
services:
doeatfit-frontend:
image: ghcr.io/DoEatFit/doeatfit_front:dev
#...4. 3차 실패 (Gotcha): “Linux는 대소문자를 구분합니다”
모든 것을 수정했다고 생각했을 때, 마지막 복병을 만났습니다. 2차 실패를 해결하고 다시 빌드를 돌렸는데, 이번엔 Pull 단계부터 오류가 발생했습니다. (로그는 not_found 또는 unauthorized와 유사하게 표시되었습니다)
[실패 로그 3]
+ docker --context remotedocker pull ghcr.io/DoEatFit/doeatfit_front:dev
Error response from daemon: manifest for ghcr.io/DoEatFit/doeatfit_front:dev not found4-1. 원인 분석
분명 Push는 ghcr.io/DoEatFit/doeatfit_front:dev로 성공했는데, Pull이 실패했습니다.
원인은 대소문자였습니다.
GitHub Organization 이름은 DoEatFit (CamelCase)입니다. 하지만 GHCR(및 대부분의 Docker Registry 표준)은 경로(소유자 및 이미지 이름)를 모두 소문자(lowercase)로 처리하고 저장합니다.
-
docker push ghcr.io/DoEatFit/...(Push)- Jenkins(Linux)가 이 명령을 실행하면, Docker 클라이언트는 이 주소를 소문자로 변환하여
ghcr.io/doeatfit/...로 푸시합니다. (이 과정이 로그에는 잘 보이지 않습니다)
- Jenkins(Linux)가 이 명령을 실행하면, Docker 클라이언트는 이 주소를 소문자로 변환하여
-
docker pull ghcr.io/DoEatFit/...(Pull)-
하지만 원격 서버(Linux)에서
pull이나compose up을 실행할 때,image:정의에DoEatFit이라는 대문자가 포함되어 있으면, Docker 데몬은 대소문자를 구분하여ghcr.io/DoEatFit/...라는 이미지를 찾으려고 시도합니다. -
실제로는
ghcr.io/doeatfit/...(소문자)으로 저장되어 있으니, 이미지를 찾지 못하고manifest not found오류를 반환한 것입니다.
-
4-2. 해결 과정
Jenkinsfile과 docker-compose.yaml Credential 파일 양쪽 모두의 이미지 경로를 전부 소문자로 통일했습니다.
-
잘못된 경로 (대소문자 미구분):
ghcr.io/DoEatFit/doeatfit_front:dev(X) -
올바른 경로 (전부 소문자):
ghcr.io/doeatfit/doeatfit_front:dev(O)
5. ✍️ 최종 해결 및 워크플로우 정상화
3번의 실패 끝에 드디어 기존 파이프라인이 정상으로 돌아왔습니다.
graph RL A["GitHub Private Repo<br>(dev branch push)"] -->|Webhook| B(Jenkins) subgraph "CI: Build Server (Jenkins)" B --> C{"1. Checkout<br>(from DoEatFit/..)"} C --> D{"2. Build Image<br>(to ghcr.io/doeatfit/..)"} D --> E{"3. Push to GHCR<br>(ghcr.io/doeatfit/..)"} end subgraph "CD: Remote Server ([REMOTE_IP])" E -->|"SSH ([REMOTE_USER])"| F{"4. Pull from GHCR<br>(ghcr.io/doeatfit/..)"} F --> G{"5. Docker Compose Up<br>(image: ghcr.io/doeatfit/..)"} G --> H[("🚀<br>배포 성공")] end
조직 이름을 변경하는 간단한 작업이 Jenkinsfile, Jenkins Credential, 그리고 Linux(Docker)의 대소문자 정책이라는 세 가지 영역에 걸쳐 연쇄적인 장애를 일으켰습니다.
이번 ISS-163 장애 대응을 통해 CI/CD 파이프라인을 구성하는 요소들이 얼마나 유기적으로 연결되어 있는지, 그리고 명시적인 경로(특히 소문자!) 관리가 얼마나 중요한지 다시 한번 깨닫게 되었습니다.