18P by xguru 19일전 | favorite | 댓글 6개
  • Docker 컨테이너 이미지를 빌드할 때, Dockerfile이 Multi-Stage 구조가 아니라면 불필요한 파일이 포함될 가능성이 큼
  • 이는 이미지 크기 증가 및 보안 취약성 증가로 이어짐
  • 컨테이너 이미지에서 발생할 수 있는 “불필요한 파일”의 주요 원인을 분석하고, Multi-Stage Build를 통해 이를 해결하는 방법을 설명

이미지 크기가 커지는 원인

  • 애플리케이션은 빌드 타임과 런타임의 의존성을 가짐.
  • 빌드 타임 의존성은 런타임보다 많고 보안 취약점(CVEs)이 더 많음.
  • 같은 이미지를 빌드와 실행에 사용하면, 불필요한 빌드 타임 의존성(컴파일러, 린터 등)이 포함됨.
  • 빌드와 런타임 이미지는 분리되어야 하지만, 이를 간과하는 경우가 많음.

잘못된 Dockerfile 구조 예시

Go 애플리케이션용의 잘못된 예

FROM golang:1.23  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
CMD ["/app/binary"]  
  • golang:1.23 이미지는 컴파일용이지만, 이를 그대로 프로덕션 환경에 사용하면 전체 Go 컴파일러와 의존성까지 포함됨.
  • 이미지 크기: 800MB 이상, 800개 이상의 보안 취약점 존재.

Node.js 애플리케이션의 잘못된 예

FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
ENV NODE_ENV=production  
EXPOSE 3000  
CMD ["node", "/app/.output/index.mjs"]  
  • node_modules 폴더가 런타임에 필요 없는 개발 의존성까지 포함하게 됨.
  • npm ci --omit=dev로 수정할 수 없으며, 빌드 과정에서 필요한 개발 의존성이 제거될 수 있음.

Multi-Stage Build 이전의 Lean 이미지 제작 방법

Builder 패턴

  1. Dockerfile.build에서 애플리케이션을 빌드함:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. 빌드된 아티팩트를 호스트로 복사:
docker cp $(docker create build:v1):/app/.output .  
  1. Dockerfile.run에서 런타임 이미지를 생성:
FROM node:lts-slim  
WORKDIR /app  
COPY .output .  
CMD ["node", "/app/.output/index.mjs"]  
•	문제점: 여러 Dockerfile 작성, 빌드 순서 관리 필요, 추가 스크립트 요구.  

Multi-Stage Build의 이해

  • Multi-Stage Build는 Docker 내부에 Builder 패턴을 구현한 기능임.
    • 여러 FROM 명령을 사용하여 하나의 Dockerfile에서 빌드와 런타임 스테이지를 정의 가능.
    • COPY --from=<stage> 명령을 사용해 이전 스테이지에서 빌드한 파일을 가져옴.

Multi-Stage Dockerfile 예시 (Node.js)

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM node:lts-slim AS runtime  
WORKDIR /app  
COPY --from=build /app/.output .  
ENV NODE_ENV=production  
CMD ["node", "/app/.output/index.mjs"]  
  • COPY --from=build로 빌드된 아티팩트를 직접 복사함으로써, 호스트를 거치지 않고 파일을 이동 가능.

Multi-Stage Build 실전 예시

React 애플리케이션

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM nginx:alpine  
COPY --from=build /app/build /usr/share/nginx/html  
ENTRYPOINT ["nginx", "-g", "daemon off;"]  
  • React 애플리케이션은 빌드 후 정적 파일이 되며, Nginx로 서빙 가능.

Go 애플리케이션

# Build stage  
FROM golang:1.23 AS build  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
  
# Runtime stage  
FROM gcr.io/distroless/static-debian12:nonroot  
COPY --from=build /app/binary /app/binary  
ENTRYPOINT ["/app/binary"]  
  • distroless 이미지를 사용해 최소화된 런타임 환경 제공.

Java 애플리케이션

# Build stage  
FROM eclipse-temurin:21-jdk-jammy AS build  
WORKDIR /build  
COPY . .  
RUN ./mvnw package -DskipTests  
  
# Runtime stage  
FROM eclipse-temurin:21-jre-jammy  
COPY --from=build /build/target/app.jar /app.jar  
CMD ["java", "-jar", "/app.jar"]  
  • 빌드에는 JDK를 사용하고, 런타임에는 더 가벼운 JRE 사용.

결론

  • Multi-Stage Build는 빌드와 런타임 환경을 분리하여 불필요한 개발 의존성으로 인한 이미지 크기 증가를 방지함
  • 이를 통해 이미지 크기를 줄이고, 보안을 강화하며, 빌드 프로세스를 간소화할 수 있음
  • Multi-Stage Build는 효율적인 컨테이너 이미지를 만들기 위한 표준적인 방법이며, 고급 기능(예: 분기 조건, 빌드 중 유닛 테스트)도 지원함

자바의 경우 jlink가 9버전부터 도입됐지만 의존 모듈을 jdeps 로 찾아서 명시해줘야 하는 등 사용성이 안좋습니다. 사람들이 저런 방법을 모르거나 JRE를 찾는 걸 보면 자바 도구의 홍보가 부족한 것 같고, 명령어 하나로 JRE가 나오게 개선이 필요해 보입니다

저렇게 쓰고 있긴한데, 빌드 시간이 오래 걸리는건 단점 같아요

빌드시간은 차이가 없어야할거에요. 차이가 있다면 잘못 설정한것!

아, 그렇군요!

전략에 따라서 한 스테이지를 통으로 캐시할 수도 있어서 전 오히려 빌드 시간이 단축되더라고요!

Docker에 대해 좀 더 알아봐야하겠네요!