무색
기술블로그
에세이
연구
프로덕트
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
CI/CD 빌드가 느려서 3곳을 동시에 고쳤다 — stipple key visual
museck 만들기
2026. 1. 11.

CI/CD 빌드가 느려서 3곳을 동시에 고쳤다

dockerci-cdbuildkitgitea-actionsoptimization

무색 홈페이지 CI/CD 파이프라인을 구축한 뒤로 빌드가 좀 느리다는 느낌이 계속 있었다. push 한 번 하면 빌드부터 배포까지 체감상 꽤 걸렸고, 워크플로우 로그를 뜯어보니 느린 구간이 한두 곳이 아니었다. 한 번에 다 고치기보다 4개 커밋으로 나눠서 하나씩 잡아나갔다.

어디가 느린지부터 파악했다

이전 글에서 Gitea Actions + DinD로 CI/CD를 처음 만들었는데, 그때는 "일단 돌아가게" 만드는 데 집중했다. 이번엔 빌드 로그를 시간 단위로 쪼개서 병목을 찾았다.

크게 세 영역에서 개선할 게 보였다.

  1. 워크플로우: apt-get이 매번 불필요한 패키지까지 설치하고 있었고, git clone이 전체 이력을 받고 있었다
  2. Dockerfile: 캐시를 전혀 활용하지 않아서 매번 처음부터 빌드하고 있었다
  3. .dockerignore: 빌드에 필요 없는 파일(.husky, .claude, scripts 등)이 컨텍스트에 포함되고 있었다

워크플로우 최적화: 불필요한 것부터 덜어냈다

DinD 러너 환경에는 ca-certificates, curl, git이 이미 깔려 있다. 처음 워크플로우를 만들 때는 이걸 몰라서 다 다시 설치했는데, 지금 보니 시간 낭비였다. 이미 있는 건 빼고 docker-ce-cli와 docker-buildx-plugin만 설치하도록 바꿨다.

--no-install-recommends 옵션도 추가했다. 추천 패키지까지 끌려오면 설치 시간이 꽤 늘어난다.

git clone도 --depth 1로 바꿨다. 빌드할 때 커밋 이력은 필요 없으니 최신 커밋만 가져오면 된다. 레포 크기가 크지 않아서 극적인 차이는 아니지만 안 할 이유도 없다.

3단계 캐시 전략

빌드 시간을 가장 크게 줄인 건 캐시 전략이다. 세 가지 레이어로 캐시를 쌓았다.

인라인 캐시 (레지스트리 기반)

buildcache라는 별도 태그로 이전 빌드 이미지를 레지스트리에 저장해두고 다음 빌드에서 --cache-from으로 참조한다. BUILDKIT_INLINE_CACHE=1을 켜면 빌드된 이미지에 캐시 메타데이터가 내장되어서 레지스트리에서 바로 캐시로 쓸 수 있다.

- name: Pull cache image
  run: docker pull gitea.xssh.org/admin/museck:buildcache || true

- name: Build
  env:
    DOCKER_BUILDKIT: '1'
  run: |
    docker build \
      --cache-from gitea.xssh.org/admin/museck:buildcache \
      --build-arg BUILDKIT_INLINE_CACHE=1 \
      -t gitea.xssh.org/admin/museck:$TAG \
      -t gitea.xssh.org/admin/museck:latest \
      -t gitea.xssh.org/admin/museck:buildcache .

|| true가 붙어 있는 이유는 첫 빌드에서는 캐시 이미지가 없어서 pull이 실패하기 때문이다. 실패해도 빌드는 계속 진행되어야 한다.

BuildKit 마운트 캐시

Dockerfile에서 --mount=type=cache를 쓰면 특정 디렉토리를 빌드 간에 공유할 수 있다. pnpm store와 Next.js 빌드 캐시 두 곳에 적용했다.

# pnpm 패키지 캐시
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    corepack enable pnpm && pnpm install --frozen-lockfile

# Next.js 빌드 캐시
RUN --mount=type=cache,target=/app/.next/cache \
    corepack enable pnpm && pnpm build

pnpm install은 패키지를 다운로드하는 시간이 대부분이라 store 캐시가 있으면 확 줄어든다. Next.js 빌드 캐시도 변경 없는 페이지를 다시 빌드하지 않게 해줘서 효과가 크다.

docker push 병렬화

빌드 후 이미지를 레지스트리에 푸시하는데 3개 태그를 올려야 한다. SHA 태그, latest, buildcache. 순차적으로 하면 네트워크 I/O만 기다리는 시간이 3배다.

- name: Push images
  run: |
    docker push gitea.xssh.org/admin/museck:$TAG &
    docker push gitea.xssh.org/admin/museck:latest &
    docker push gitea.xssh.org/admin/museck:buildcache &
    wait

&로 백그라운드 실행하고 wait로 전부 끝날 때까지 기다린다. Docker 이미지 레이어는 공유되니까 실제 전송량은 1회분에 가깝고 레지스트리 API 호출만 병렬화되는 셈이다.

.dockerignore 정리

Docker 빌드는 컨텍스트를 먼저 데몬으로 보내는데, 불필요한 파일이 많으면 이 과정이 느려진다. 빌드에 필요 없는 디렉토리를 .dockerignore에 추가했다.

.husky
.claude
.mcp.json
scripts
tests
.gitea

Husky 훅 설정, Claude MCP 설정, 스크립트, 테스트, Gitea 워크플로우 파일 등은 런타임에서 쓰지 않는다. 컨텍스트에서 빼면 전송 시간이 줄고 캐시 무효화도 방지된다.

buildx 플러그인 빼먹고 삽질한 이야기

BuildKit 마운트 캐시를 쓰려면 DOCKER_BUILDKIT=1이 필요하다. 환경변수를 설정했는데 빌드가 실패했다. --mount=type=cache 구문을 인식 못 하는 거였다.

알고 보니 DinD 환경에서는 docker-buildx-plugin을 별도로 설치해야 했다. 로컬에서는 Docker Desktop에 이미 포함되어 있어서 의식하지 못했는데, CI 러너는 최소 설치 환경이라 빠져 있었다.

# Before: buildx 없이 docker-ce-cli만 설치
apt-get install -y docker-ce-cli

# After: buildx 플러그인 추가
apt-get install -y --no-install-recommends docker-ce-cli docker-buildx-plugin

CI 환경은 로컬과 다르다. 당연히 있을 거라고 생각하면 안 된다.

점진적으로 고쳤다

이 최적화를 한 커밋에 몰아넣지 않고 4개로 나눴다. apt-get 정리, git clone 최적화, Docker 캐시 도입, 병렬 push 순서대로. 각 커밋마다 빌드를 돌려보고 어디서 얼마나 줄었는지 확인할 수 있어서 좋았다.

최적화를 한꺼번에 적용하면 뭐가 효과 있었는지 알 수 없다. 한 번에 하나씩 바꾸고 측정하는 게 결국 빠르다.

정리

Docker 빌드 최적화는 워크플로우, Dockerfile, .dockerignore 세 곳을 함께 봐야 한다. 어느 한 곳만 고치면 효과가 제한적이다.

  • 러너 환경에 이미 있는 패키지를 파악하고 불필요한 설치를 제거
  • 인라인 캐시 + 마운트 캐시로 빌드 간 재사용 극대화
  • I/O 바운드 작업(push)은 병렬화
  • .dockerignore로 빌드 컨텍스트 최소화

CI/CD 파이프라인은 한 번 만들면 끝이 아니라 계속 다듬어야 한다. "느리다"는 느낌이 들 때가 개선할 타이밍이다.

자주 묻는 질문

Docker 빌드에서 BuildKit 마운트 캐시는 어떻게 설정하나요?
Dockerfile에서 RUN --mount=type=cache,target=/path 구문을 사용합니다. pnpm store와 .next/cache에 적용하면 빌드 간 패키지와 빌드 결과를 재사용할 수 있습니다. DinD 환경에서는 docker-buildx-plugin을 별도 설치해야 합니다.
Docker 이미지 push를 병렬화하려면 어떻게 하나요?
셸에서 각 docker push 명령 뒤에 &를 붙여 백그라운드로 실행하고 wait로 전부 완료될 때까지 기다립니다. 이미지 레이어는 공유되므로 실제 전송량은 1회분에 가깝고 레지스트리 API 호출만 병렬화됩니다.
CI/CD 빌드 최적화를 한 번에 하지 않고 나눠서 하는 이유는?
최적화를 한꺼번에 적용하면 어떤 변경이 효과가 있었는지 알 수 없습니다. 커밋 단위로 나눠서 각각 빌드를 돌려보면 개선 효과를 정확히 측정할 수 있어 점진적 최적화가 가능합니다.
museck 만들기(9/10)
Prev

PayloadCMS Lexical 에디터에 코드 블록 넣기: 4개 레이어 삽질기

Next

CI에서 터지기 전에 잡자: husky + lint-staged 설정기