며칠 전, 평소처럼 배포를 진행하는데 이상하게 계속 실패하는 상황이 발생했다. 에러 로그를 확인해보니 디스크 공간 부족 때문이었다.
그래서 docker system prune -a 명령어로 도커 캐시도 싹 정리해봤지만, 여전히 디스크 용량은 차 있는 상태였다. 도커 이미지 용량도 확인해보니, 무려 12GB나 되는 괴물 같은 크기였다.
이미지를 줄이지 않으면 안 되겠다는 생각에, 결국 Dockerfile을 처음부터 전면적으로 리팩토링하게 되었고, 도커 레이어까지 직접 수작업으로 정리하면서 문제를 해결하게 되었다.
우선, 내가 사용하던 기존 Dockerfile은 아래와 같은 구조였다.
FROM node:18
WORKDIR /home/ubuntu/프로젝트명
COPY package*.json ./
RUN npm install
RUN npm ci --only=production
# 시스템 라이브러리 및 sharp 설치
RUN apt-get update && apt-get install -y libvips-dev libjpeg-dev libpng-dev
RUN apt-get update \
&& apt-get install -y libvips-dev python3 make g++ \
&& yarn add sharp \
&& apt-get remove -y libvips-dev python3 make g++ \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
처음에는 별다른 이상이 없어 보였지만, 자세히 들여다보니 여러 문제가 숨어 있었다.
풀 버전 베이스 이미지 사용
node:18은 기본적으로 Debian 기반의 풀 버전 이미지다. 이 이미지에는 많은 시스템 도구들이 포함되어 있는데, 런타임에 불필요한 것들도 많기 때문에 이미지 용량이 자연스럽게 커질 수밖에 없다.
개발용 의존성이 같이 포함됨
@types/* 같은 개발용 패키지들이 프로덕션 이미지에도 그대로 들어가 있었다. 이는 런타임에는 필요하지 않기 때문에 완전히 불필요한 공간 낭비였다.
멀티 스테이지 빌드를 사용하지 않음
빌드 도구와 실제 실행 코드가 하나의 이미지에 섞여 있었다. 결국 빌드에 필요한 라이브러리나 컴파일러 등이 이미지에 그대로 남게 되어버린 것이다.
COPY . .의 위치 문제
프로젝트 전체를 이미지 후반부에 복사하다 보니 .git, 로그 파일, 기타 빌드 결과물 등이 전부 이미지에 포함되었을 가능성이 높았다.
불필요한 RUN 명령어 중복 사용
npm install, npm ci, npm i --save-dev 등 비슷한 명령어가 여러 번 실행되면서 Docker 레이어가 계속 쌓이게 되었고, 결과적으로 이미지가 무겁게 됐다.
이미지 용량을 줄이기 위해 가장 먼저 적용한 전략은 멀티 스테이지 빌드였다.
빌드와 실행 환경을 완전히 분리해서, 런타임에는 꼭 필요한 파일만 남기는 방식이다.
1단계: 빌드 스테이지
FROM node:18-alpine AS builder
WORKDIR /home/ubuntu/프로젝트명
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production
여기서는 node:18-alpine을 사용했다. Alpine은 초경량 리눅스 배포판이라 이미지 사이즈 자체가 훨씬 작다.
또 npm install 대신 npm ci로 설치 속도를 높였고
마지막에는 npm prune --production으로 개발용 패키지를 날려버렸다.
2단계: 러너 스테이지
FROM node:18-alpine AS runner
WORKDIR /home/ubuntu/프로젝트명
COPY --from=builder /home/ubuntu/프로젝트명/node_modules ./node_modules
COPY --from=builder /home/ubuntu/프로젝트명/package.json ./package.json
COPY --from=builder /home/ubuntu/프로젝트명/.next ./.next
COPY --from=builder /home/ubuntu/프로젝트명/public ./public
COPY --from=builder /home/ubuntu/프로젝트명/next.config.js ./
COPY --from=builder /home/ubuntu/프로젝트명/next-i18next.config.js ./
COPY .env.production .env.production
EXPOSE 3000
CMD [ "npm", "start" ]
최종 실행 환경에는 .next, node_modules, public, 설정 파일들만 포함시켰다.
소스코드나 개발 도구, 타입 파일 등은 포함되지 않도록 했다.
이미지 크기가 12GB에서 무려 2GB로 줄어들었다!


도커 이미지 최적화만으로도 많은 용량이 줄긴 했지만, /var/lib/docker/overlay2를 보니 여전히 디스크 공간을 꽤 차지하고 있었다. 이 디렉토리는 도커의 레이어들이 저장되는 곳인데, prune 명령어로도 지워지지 않는 레이어들이 남아 있는 상태였다.
이 중 일부는 이미 삭제된 이미지의 흔적들이었고, 이를 고아 레이어(Orphan Layer) 라고 부른다.
도커 이미지가 제거되었지만 해당 레이어 참조가 제대로 해제되지 않은 경우
도커 빌드 실패 후 남은 중간 레이어
도커 데몬이 비정상적으로 종료되어 정리되지 않은 레이어
이래는 고아 레이어를 직접 제거하는 방법이다
1. /var/lib/docker/overlay2에서 용량이 큰 도커 레이어 목록 확인
sudo find /var/lib/docker/overlay2 -maxdepth 1 -type d -exec du -sh {} \; | sort -hr | head -20
위 명령어를 입력하면 아래와 같이 출력된다. 어마어마하게 큰 레이어가 존재하는 모습이다.
55G /var/lib/docker/overlay2
6.7G /var/lib/docker/overlay2/dobbuajkdmn0mwoqb7ukfa2du (=캐시 ID)
...
/var/lib/docker/
├── overlay2/
│ ├── <캐시 ID>/ # 실제 레이어의 파일 시스템(도커가 특정 레이어를 위해 만든 임시 디렉토리)
│ ├── ...
2. 해당 캐시 ID가 어떤 레이어에 속하는지 추적
이 과정이 필요한이유는 레이어의 캐시 ID에 매핑되는 레이어 해시값을 찾아야 하기 때문이다.
/var/lib/docker/image/overlay2/layerdb/ 경로는 각 레이어에 대한 메타데이터 정보를 가지고 있다.
sudo grep -rl <캐시 ID> /var/lib/docker/image/overlay2/layerdb/
위 명령어를 입력하면
/var/lib/docker/image/overlay2/layerdb/sha256/0c4ae8d61800.../cache-id
../layerdb/sha256/해시값/cache-id 형태로 출력된다.
/var/lib/docker/image/overlay2/layerdb/sha256/0c4ae8d61800590da0f06.../
├── cache-id # 실제 레이어가 저장된 overlay2/<캐시 ID>를 가리킴(=실제 파일 저장 위치)
├── parent # 이 레이어가 어떤 레이어에서 파생되었는지
├── diff # 파일시스템 diff에 대한 정보
3. 추적된 레이어 해시가 현재 이미지에서 사용되고 있는지 확인
/var/lib/docker/image/overlay2/imagedb/content/sha256/는 현재 Docker에 등록된 모든 이미지의 메타데이터가 저장되어 있다.
sudo grep -rl <해시값> /var/lib/docker/image/overlay2/imagedb/content/sha256/
결과가 없으면, 현재 사용되고 있지 않다는 의미이다.
4. 아무 이미지에서도 사용하지 않는다면, 고아 레이어로 간주하고 삭제
sudo rm -rf /var/lib/docker/overlay2/<캐시 ID>
10GB 밖에 남지 않았던 디스크 공간이 총 30GB로 증가하여 문제없이 배포를 성공했다
Docker를 쓰다 보면 prune만으로는 해결되지 않는 숨은 용량 문제들이 종종 생긴다.
특히 이미지 크기 최적화는 단순히 디스크를 아끼는 걸 넘어, 빌드 속도, 배포 시간, 다운로드 트래픽까지 영향을 주는 요소이기 때문에 무시할 수 없다.
그리고 디스크 용량이 줄어들지 않는다면, /var/lib/docker/overlay2도 꼭 들여다보자. 진짜 범인은 그 안에 있을지도 모른다...!