목표: 빌드 시간↓ / 전송량↓ / 실패율↓ / 배포 속도↑

1) 병목을 먼저 정의

대부분 여기서 터집니다.

  • 빌드 캐시 미사용: 매번 gradle deps / npm install 재실행
  • Docker Hub rate limit: 베이스 이미지 pull에서 429, 인증 꼬임
  • 레이어 설계 구림: 코드 한 줄 바뀌어도 “전체 레이어 재빌드”
  • Registry가 멀다/느리다: 온프레미스가 해외 Registry를 매번 pull
  • 태그 전략 부재: 롤백/디버깅 불가, “latest”만 씀

해결책은 크게 3축입니다.

  1. 빌드 최적화 (Dockerfile)
  2. CI 최적화 (Actions 캐시 + Buildx)
  3. Registry/배포 최적화 (GHCR 또는 사내 Registry + 미러/프록시)

2) Dockerfile 최적화 (레이어 캐시가 먹게 만들기)

핵심 규칙

  • “자주 바뀌는 것”은 아래로, “덜 바뀌는 것”은 위로
  • 의존성 설치 레이어를 코드 복사보다 먼저
  • 멀티스테이지 + 런타임 슬림화

Spring/Gradle 예시(정석)

# syntax=docker/dockerfile:1.7
FROM gradle:8.7-jdk21-alpine AS builder
WORKDIR /app

# 1) 의존성 캐시용 파일 먼저
COPY gradle gradle
COPY gradlew settings.gradle* build.gradle* ./
RUN --mount=type=cache,target=/home/gradle/.gradle \
    ./gradlew dependencies --no-daemon || true

# 2) 소스는 나중에
COPY src src
RUN --mount=type=cache,target=/home/gradle/.gradle \
    ./gradlew bootJar --no-daemon -x test

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]# syntax=docker/dockerfile:1.7
FROM gradle:8.7-jdk21-alpine AS builder
WORKDIR /app

# 1) 의존성 캐시용 파일 먼저
COPY gradle gradle
COPY gradlew settings.gradle* build.gradle* ./
RUN --mount=type=cache,target=/home/gradle/.gradle \
    ./gradlew dependencies --no-daemon || true

# 2) 소스는 나중에
COPY src src
RUN --mount=type=cache,target=/home/gradle/.gradle \
    ./gradlew bootJar --no-daemon -x test

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]

 

 

포인트:

  • BuildKit의 --mount=type=cache가 Gradle 캐시를 CI에서도 활용 가능하게 해줌.
  • dependencies 레이어가 살아있으면 코드만 바뀔 때 빌드가 훨씬 빨라짐.

3) GitHub Actions 최적화 (Buildx + Registry Cache)

“진짜” 체감 나는 2개

  • docker/build-push-action + cache-to/cache-from
  • metadata-action으로 태그/라벨 표준화

예시: GHCR(추천)로 푸시 + Registry 캐시

name: build-and-push

on:
  push:
    branches: [ "main" ]

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha
            type=raw,value=latest
            type=ref,event=branch

      - name: Build & Push (with registry cache)
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=maxname: build-and-push

on:
  push:
    branches: [ "main" ]

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha
            type=raw,value=latest
            type=ref,event=branch

      - name: Build & Push (with registry cache)
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max

 

왜 GHCR 추천?

  • Docker Hub 대비 rate limit 스트레스가 훨씬 적고, GitHub Actions와 권한/토큰이 자연스럽게 연결됨.
  • 사내/개인 프로젝트에 특히 안정적.

4) Registry 선택 전략 (온프레미스 배포 기준)

A안) GHCR 사용 (가장 간단, 운영 부담 최소)

  • CI → GHCR push
  • 온프레미스 서버 → GHCR pull

단점: 사내 네트워크가 해외로 나가야 함(보안 정책에 따라 막힐 수 있음)

B안) 사내 Registry 운영 (가장 빠름, 가장 통제 가능)

  • 온프레미스 내부에 registry:2 띄우고
  • CI는 거기로 푸시하거나, 내부가 외부 pull을 캐시하도록 구성

5) 온프레미스 “Pull-through Cache”로 속도/안정성 올리기

외부 Registry(Docker Hub, GHCR 등)를 매번 땡기지 말고 사내 Registry를 캐시 프록시로 쓰는 방식.

docker registry(2) 프록시 설정 예시

/opt/registry/config.yml

 
 
version: 0.1
proxy:
remoteurl: https://registry-1.docker.io
storage:
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
 

실행:

 
 
docker run -d --name registry \
-p 5000:5000 \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml \
-v /opt/registry/data:/var/lib/registry \
registry:2
 

이제 온프레미스 Docker daemon에 “미러”로 지정하면,
베이스 이미지 pull이 내부 캐시로 빨라지고 rate limit도 완화됩니다.

/etc/docker/daemon.json

 
 
{
"registry-mirrors": ["http://YOUR_REGISTRY_IP:5000"]
}
 
 
 
sudo systemctl restart docker
 

6) 태그/롤백 전략 (DevOps 관점 필수)

최소 3종은 고정으로 가세요.

  • :sha-<gitsha> (배포/롤백의 기준)
  • :latest (편의용, 운영 기준 X)
  • :main 또는 :release-YYYYMMDD (환경별 트래킹)

운영 배포는 “sha 태그” 또는 “digest 고정”이 안전합니다.

  • docker pull ghcr.io/org/app@sha256:... (변경 불가)

7) Registry 운영 최적화(사내 운영 시)

  • 디스크: 레지스트리 데이터는 SSD 권장
  • 정리:
    • 오래된 태그 삭제 정책
    • garbage-collect(레지스트리 GC) 주기 운영
  • 네트워크:
    • 온프레미스 내부망에서 접근되게 배치
    • 가능하면 reverse proxy(Nginx)로 TLS 종단 + 인증

8) 체크리스트 (바로 적용)

  • Dockerfile 레이어 재구성 (의존성 먼저)
  • Actions: Buildx + cache-to/cache-from 적용
  • Registry: GHCR로 이동(또는 사내 registry 구축)
  • 온프레미스: pull-through cache 또는 registry mirror 적용
  • 태그: sha 기반으로 배포/롤백 체계 고정
LIST

+ Recent posts