← 전체 글로 돌아가기

Docker

Docker 이미지 크기를 줄일 때 multi-stage 빌드가 항상 정답은 아니었던 이유

Node.js 컨테이너를 줄이며 alpine과 slim의 호환성, 캐시, 운영 조건을 비교한 글입니다.

1GB 이미지에서 시작했다

소스와 개발 의존성을 모두 넣은 Node.js API 이미지가 1GB에 가까웠다. 곧장 alpine으로 바꾸려 했지만 네이티브 모듈 설치가 실패했다. 작은 베이스 이미지와 실행 호환성을 같은 기준으로 본 것이 첫 실수였다.

빌드와 실행을 분리한다

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

alpine은 더 작았지만 일부 패키지가 musl 환경에서 다르게 동작했다. slim은 조금 컸어도 운영 환경과 디버깅 차이가 작아 최종 선택으로 남겼다.

캐시와 비밀 값

# .dockerignore
node_modules
.git
.env*
.next
coverage

package*.json을 먼저 복사하면 소스가 바뀌어도 의존성 레이어를 재사용한다. 환경 변수 파일은 이미지에 넣지 않고 실행 시 주입한다.

비교할 숫자

docker build -t myapp:slim .
docker image inspect myapp:slim --format '{{.Size}}'
docker run --rm myapp:slim node -e "console.log(process.platform, process.arch)"

이미지 크기만 보지 말고 빌드 성공, 시작, 취약점 검사, 네이티브 모듈 동작을 함께 기록한다. 가장 작은 이미지보다 재현 가능하고 장애 때 확인 가능한 이미지가 더 적합했다.