Develop
Docker 멀티스테이지 빌드로 Node.js 이미지 크기를 줄이는 방법
그냥 빌드하면 1.5GB를 초과하며 비대해지기 쉬운 Node.js / Next.js 프로젝트의 Docker 컨테이너 이미지를, 빌드 전용 환경과 경량 런타임 셸을 분리하는 멀티스테이지 빌드를 통해 10분의 1 크기로 다이어트시키는 팁을 정리했습니다.

- ·멀티스테이지 빌드: 하나의 Dockerfile에 여러 FROM 지시어, 최종 이미지에는 마지막 스테이지만 포함
- ·node:alpine: Debian 기반 node 이미지 대비 약 3배 작은 Alpine Linux 기반 경량 이미지
- ·COPY --from=builder: 이전 스테이지에서 빌드된 파일만 복사해 최종 이미지 구성
- ·.dockerignore: node_modules, .git 등 불필요한 파일을 빌드 컨텍스트에서 제외
Next.js 애플리케이션 코드를 Docker로 첫 패키징해서 로컬에 띄웠을 때, 출력된 이미지 용량이 무려 1.5GB에 육박하는 괴물 같은 사이즈를 보고 경악을 금치 못했습니다. 타겟 컴파일을 위해 집어넣었던 무거운 개발 의존 라이브러리(devDependencies)와 캐시 찌꺼기가 몽땅 패키지에 압축 매핑된 탓이었습니다. 이를 개선하고자 `FROM` 구문을 나누어 쓰는 멀티스테이지 기법을 이식했습니다. 빌드용 builder 컨테이너에서 무거운 빌드 과정을 처리해 버리고, 최종 런타임 runner 이미지에는 컴파일된 standalone 산출물만 안전 복사해 엮으니, 이미지 크기가 단숨에 180MB 대역으로 축소되는 놀라운 이식 다이어트 효과를 쟁취했습니다.
1. 컨테이너 이미지 크기와 비용 최적화
용량이 큰 Docker Node.js 이미지의 비효율성과 보안 위험 극복하기
Docker 이미지를 단일 단계로 대충 말아두면 최종 가상 머신 디렉토리 안에 tsc 컴파일러, 웹팩 빌더, 린트 도구 등 오직 개발 시에만 쓰이고 프로덕션 서버 실행 단계에서는 일절 필요 없는 무거운 패키지들이 몽땅 상주합니다. 이는 단순히 AWS 컨테이너 레지스트리(ECR) 저장소 비용을 갉아먹는 것 이상의 트러블을 만듭니다. 이미지가 크면 배포할 때마다 서버가 기가바이트 단위의 파일을 다운받느라 트래픽을 허비하여 오토스케일링 대응 속도가 굼떠집니다. 더욱이, 이미지 내부에 쓸모없는 리눅스 패키지와 라이브러리 찌꺼기가 많아질수록 공인된 보안 취약점 점검(CVE) 스캔에서 지적을 당하는 침입 위험 표면(Attack Surface)도 비대해지는 악영향을 초래합니다.
2. 멀티스테이지 Dockerfile 구성
builder와 runner 스테이지로 격리하는 Docker Node.js 멀티스테이지 설계법
멀티스테이지의 핵심 동작 요령은 하나의 Dockerfile에 여러 개의 FROM 절을 개설해 활용하는 것입니다. 첫 번째 블록인 FROM node:20 AS builder 영역은 소스 코드를 컴파일하고 의존성을 긁어 모으는 공장 역할을 합니다. 무거운 빌드 명령이 마감되면, 그 아래로 두 번째 FROM node:20-alpine AS runner 절을 선언해 새로운 가벼운 가상 컨테이너를 가동시킵니다. 이후 COPY --from=builder 명령 지시어를 기재해, 오직 빌드 공장에서 완성된 순수 아웃풋 디렉토리(/app/dist 등)와 프로덕션 구동용 핵심 패키지들만 정밀 선택 복사해 옵니다. 최종 이미지 파일에는 이 두 번째 runner 환경의 최소 레이어만 남게 되므로, 첫째 공장의 껍데기는 온데간데없이 사라진 순수한 에센셜 패키지만이 컴파일되어 이미지 껍데기를 대폭 줄이게 됩니다.
# 1단계: 빌드 전용 공장 (builder)
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 2단계: 경량 런타임 환경 (runner)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
# 프로덕션 전용 라이브러리만 선별 설치
RUN npm ci --omit=dev
# 이전 builder 공장에서 나온 dist 결과물만 전송 링킹
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]standalone 옵션과 연계하여 Docker Node.js 빌드 최적화하기
Next.js 프레임워크 프로젝트 환경이라면 next.config.ts 파일 내부의 컴파일러 옵션 설정 객체에 output: 'standalone' 옵션을 심어주는 것이 궁극의 치트키입니다. 빌드를 올릴 때 넥스트가 소스 코드를 읽어 들여 런타임 구동에 필요한 핵심 자바스크립트 모듈 파일들을 .next/standalone 이라는 폴더 안으로 안전하게 자생 응축시켜 주기 때문이죠. 이 standalone 폴더 안에는 무거운 넥스트 내부 코어 패키지들까지 컴파일 복사되어 담겨 있으므로, runner 스테이지에서는 호스트 디바이스의 무거운 node_modules를 통째 복사할 것 없이 이 standalone 디렉토리 하나만 쏙 복사하여 서버 구동용 server.js 파일을 실행해 주면 빌드가 초경량으로 끝납니다.
3. 빌드 속도 향상과 무시 필터 설정
.dockerignore를 통한 Docker 빌드 컨텍스트 전송 시간 절약 요령
멀티스테이지와 별개로, 프로젝트 루트에 .dockerignore 설정 파일을 파서 기입해 두는 것도 컨테이너 이미지 개발 생산성 향상에 막대한 기여를 단행합니다. 이 파일이 없으면 docker build 명령이 작동하는 첫 단계에서 호스트 개발 장비의 무겁디무거운 node_modules 폴더와 로컬 빌드 테스트 찌꺼기(.next, .git 폴더) 전체를 도커 백그라운드 가상 데몬으로 복사 전송하는 'Sending build context to Docker daemon' 지연 시간이 걸려 몇 분간 터미널이 멈춥니다. 무시 목록에 이 3가지를 명확히 엮어두는 것만으로 빌드 시작 대기 시간을 단 1초 미만으로 압축할 수 있습니다.
캐시 마운트 기법을 적용해 Docker Node.js 빌드 속도 향상시키기
매번 도커 빌드가 돌 때마다 아무리 캐싱이 된다 한들 수많은 npm 패키지들을 다운받는 과정은 큰 네트워크 지연을 초래합니다. 이때 Docker BuildKit 엔진이 제공하는 캐시 마운트 명령어 스펙을 차용해 RUN --mount=type=cache,target=/root/.npm npm ci 형태로 구문을 개조해 주면, 빌드 대상 파일이 변경되어 의존성을 다시 깔아야 하는 불행한 상황에서도 도커 로컬 캐시 디스크 영역에 적재되어 있던 백업 패키지를 가져다 부착하므로 빌드 속도가 경이롭게 고속 질주하게 조킵니다.
자주 묻는 질문
node:alpine 기반 경량 이미지를 쓸 때 유의해야 할 호환성 에러가 있나요?+
Alpine Linux는 용량 경량화를 위해 표준 glibc 컴파일 라이브러리 대신 musl libc를 기본 뼈대로 탑재하고 있습니다. 이 때문에 이미지 크롭에 쓰는 `sharp` 나 암호화 툴인 `bcrypt` 등 C/C++ 네이티브 바이너리 링킹이 필요한 특정 npm 라이브러리가 런타임 로드 중 에러를 뿜으며 컴파일에 깨지기도 합니다. 이때는 Alpine 대신 조금 더 호환성이 보장된 `node:slim` 이미지를 대안으로 채택하시면 평화롭게 구축됩니다.
로컬의 .env 파일을 Docker 이미지 안에 기재해서 굽고 배포해도 되나요?+
.env 시크릿 파일은 절대로 Dockerfile `COPY` 문을 통해 이미지 내부에 적재해 구우면 안 됩니다. 이미지가 외부에 유출되거나 형상 저장소에 올라갔을 때 마스터 키가 공공에 털리는 대형 사고가 납니다. 민감 값은 반드시 `.dockerignore` 에 포함하여 전송 누출을 막고, 서버 기동 시점에 ECS나 Kubernetes 환경 변수 인젝터 헬퍼를 통해 런타임 주입해 활용하시는 것이 안전합니다.
관련 글
Docker로 Jenkins 설치하고 초기 설정까지 완료하는 방법
운영체제 환경의 꼬임 없이 깔끔하게 Docker 컨테이너 위에 Jenkins를 실행하는 방법과 데이터 휘발을 막기 위한 볼륨 마운트 전략, 초기 어드민 인증을 푸는 DevOps 가이드를 제공합니다.
Docker Compose로 Node.js 개발 환경을 구성하는 방법 — 앱과 DB를 한 번에 올리는 방법
Node.js 앱과 PostgreSQL을 docker-compose.yml 하나로 묶어서 실행하면 팀원 누구나 동일한 개발 환경을 docker compose up 한 줄로 구성할 수 있습니다. 볼륨, 핫 리로드, 환경변수 설정까지 정리했습니다.
Nginx 리버스 프록시로 Node.js 앱을 도메인에 연결하는 방법
Node.js 앱을 직접 80 포트로 띄우는 대신 Nginx를 리버스 프록시로 앞에 세우면 도메인 연결과 HTTPS 적용이 깔끔해진다. 설정 파일 작성부터 Certbot SSL 적용까지 정리했다.