
Docker 컨테이너 이미지를 저장·관리·배포하는 프라이빗 레지스트리 서비스.
NKS와 연동되어 컨테이너 기반 배포 파이프라인의 중심축이 된다.
Docker Hub의 프라이빗 버전 + NHN Cloud 인증 체계 결합이라고 보면 된다.
| Docker Hub 사용 시 문제 | NCR |
|---|---|
| 소스 코드가 담긴 이미지가 공개될 위험 | 프라이빗 + NHN Cloud 인증 |
| Pull rate limit (무료 계정 제한) | 제한 없음 |
| 네트워크 지연 (해외 리전) | 같은 리전 → 빠른 pull |
| 인터넷 게이트웨이 필수 | Private URI로 폐쇄망에서도 사용 가능 |
용어를 정확히 구분해야 콘솔에서 헤매지 않는다.
레지스트리 (Registry)
└─ 이미지 (Image) 예: my-api
└─ 아티팩트 (Artifact) 예: sha256:a1b2c3... ← 실체
└─ 태그 (Tag) 예: 1.0.3, latest ← 별명
이미지를 보관·관리하는 최상위 단위. 모든 이미지는 반드시 하나의 레지스트리에 속한다.
레지스트리별로 웹훅 같은 관리 설정을 지정한다.
이름 규칙: 영문 소문자 또는 숫자로 시작, 이후 소문자·숫자·_·.·- 사용 가능. 최대 255자.
아티팩트들의 집합. docker push / docker pull로 다룬다. 레지스트리 내에서 이름이 유일해야 한다.
실제 빌드 결과물. 하나 이상의 레이어를 가지며, 매니페스트의 해시값인 다이제스트(digest) 로 식별된다.
태그가 없을 수도, 여러 개일 수도 있다.
저장 공간 최적화: 여러 아티팩트가 공유하는 공통 레이어는 한 번만 저장된다.
그래서 Spring Boot 이미지를 100번 push해도 실제 용량은 100배가 되지 않는다.
단, Dockerfile 레이어 순서를 잘 짜야 이 이점을 누린다 (아래 5번 참고).
아티팩트를 구분하는 식별자. 사용자가 버전을 부여하는 수단.
이름 규칙: 영문 소문자·숫자·_로 시작, 이후 소문자·숫자·_·.·-. 최대 128자.
| 태그 | 다이제스트 | |
|---|---|---|
| 예시 | my-api:1.0.3 | my-api@sha256:a1b2c3... |
| 성격 | 가변. 다른 아티팩트에 다시 붙일 수 있음 | 불변. 내용이 바뀌면 값도 바뀜 |
| 용도 | 사람이 읽기 쉬움 | 재현 가능한 배포 |
latest를 쓰면 안 되는 이유image: my-api:latest # x
kubectl apply를 해도 이미지 태그 문자열이 그대로면 K8s가 변경 없음으로 판단해 롤아웃이 안 일어난다latest가 뭐였는지 알 수 없음image: my-api:1.0.3 # 최소한 이렇게
image: my-api@sha256:a1b2c3... # 완전히 재현 가능
태그 전략 예시
1.0.3 # 시맨틱 버전 (릴리스)
1.0.3-a3f9c21 # 버전 + Git 커밋 SHA (추적 가능)
main-a3f9c21 # 브랜치 + 커밋 SHA (개발용)
docker CLI 그대로 사용| 기능 | 설명 | 언제 쓰나 |
|---|---|---|
| 리전 간 복제 | 레지스트리를 다른 리전으로 복제 | 멀티 리전 서비스, DR |
| Private URI | 인터넷 게이트웨이 없는 인스턴스에서도 접근 | 보안 요구사항이 높은 폐쇄망 클러스터 |
| 이미지 캐시 | 외부 레지스트리 이미지를 캐싱 | Docker Hub rate limit 회피, pull 속도 개선 |
| 취약점 스캐닝 | 이미지 내 알려진 CVE 탐지 | CI에서 심각도 High 이상이면 배포 차단 |
| 이미지 신뢰(Trust) | 서명된 이미지만 허용 | 공급망 공격 방어 |
| 웹훅 | push 등 이벤트 발생 시 외부 호출 | CI/CD 자동 트리거 |
# 1. 로그인 (콘솔에서 발급받은 계정/토큰 사용)
docker login <레지스트리주소>
# 2. 빌드 — 태그를 레지스트리 경로로 지정
docker build -t <레지스트리주소>/my-api:1.0.3 .
# 3. 푸시
docker push <레지스트리주소>/my-api:1.0.3
# 4. 확인
docker pull <레지스트리주소>/my-api:1.0.3
Mac Mini M4(ARM) 주의: 로컬에서 그냥 빌드하면
arm64이미지가 나온다.
NKS 노드가 x86이면exec format error로 파드가 죽는다.docker buildx build --platform linux/amd64 -t <레지스트리>/my-api:1.0.3 --push .멀티 아키텍처가 필요하면
--platform linux/amd64,linux/arm64.
프라이빗 레지스트리이므로 클러스터에 인증 정보를 알려줘야 한다.
kubectl create secret docker-registry ncr-secret \
--docker-server=<레지스트리주소> \
--docker-username=<사용자> \
--docker-password=<비밀번호또는토큰> \
-n <네임스페이스>
spec:
imagePullSecrets:
- name: ncr-secret
containers:
- name: app
image: <레지스트리주소>/my-api:1.0.3
모든 파드에 매번 적기 귀찮다면 — ServiceAccount에 붙이면 자동 적용된다:
kubectl patch serviceaccount default -n <네임스페이스> \
-p '{"imagePullSecrets":[{"name":"ncr-secret"}]}'
imagePullSecret은 네임스페이스별로 만들어야 한다.default에 만들고prod에서 쓰려 하면 실패.
ImagePullBackOff의 흔한 원인 1순위.
# ---- Build stage ----
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
# (1) 의존성 파일만 먼저 복사 → 소스만 바뀌면 이 레이어는 캐시 재사용
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon || true
# (2) 소스 복사 후 빌드
COPY src src
RUN ./gradlew bootJar --no-daemon -x test
# (3) fat jar를 레이어로 분해 (Spring Boot layertools)
RUN java -Djarmode=layertools -jar build/libs/*.jar extract --destination extracted
# ---- Runtime stage ----
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
# 비루트 사용자로 실행
RUN useradd -m -u 1001 appuser
USER appuser
# 변경 빈도가 낮은 레이어부터 복사 → 캐시 효율 극대화
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
왜 이렇게 하나
name: build-and-push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to NCR
uses: docker/login-action@v3
with:
registry: ${{ secrets.NCR_REGISTRY }}
username: ${{ secrets.NCR_USERNAME }}
password: ${{ secrets.NCR_PASSWORD }}
- name: Build & Push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ secrets.NCR_REGISTRY }}/my-api:${{ github.sha }}
${{ secrets.NCR_REGISTRY }}/my-api:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to NKS
run: |
echo "${{ secrets.KUBECONFIG }}" > kubeconfig
KUBECONFIG=kubeconfig kubectl set image deployment/my-api \
app=${{ secrets.NCR_REGISTRY }}/my-api:${{ github.sha }}
KUBECONFIG=kubeconfig kubectl rollout status deployment/my-api --timeout=5m
kubectl set image에 커밋 SHA 태그를 쓰는 게 핵심. 매번 태그가 달라지므로 롤아웃이 확실히 트리거된다.
CI가 커밋마다 push하면 이미지가 폭증한다. 정리 규칙을 걸어두자.
운영에 배포된 태그(1.0.3, prod 등)는 삭제·덮어쓰기 방지를 걸어둔다.
누군가 실수로 지우면 롤백 불가 사태가 난다.
push 후 스캔 결과를 확인하고, Critical/High가 있으면 배포를 막는 게 이상적.
베이스 이미지를 -alpine이나 distroless로 바꾸면 취약점 수가 크게 줄어든다.
| 방법 | 효과 |
|---|---|
| 멀티 스테이지 빌드 | JDK(400MB+) → JRE(200MB대) |
-jre 또는 -jre-alpine 베이스 | 큰 폭 감소 |
.dockerignore 작성 | 빌드 컨텍스트 축소 (.git, build/, node_modules 제외) |
| distroless 베이스 | 최소 크기 + 공격 표면 최소화 (단 셸이 없어 디버깅 어려움) |
docker login → build → push를 성공했다--platform linux/amd64로 빌드해야 하는 이유를 안다imagePullSecret을 만들어 NKS에서 pull에 성공했다latest 태그를 쓰면 안 되는 이유 3가지를 말할 수 있다