🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
DevOps
Docker
05-성능을 위한 Dockerfile 작성법

1. 캐싱

나만의 이미지를 만들고 싶으면 Dockerfile에다가 레시피를 작성하면 됩니다. 하지만 docker build 시간 단축, 용량절약, 보안향상을 위한 매직 레시피 같은게 있습니다.

프로젝트가 커지면 docker build 입력해서 기다리는 시간도 끔찍해지는 경우가 있습니다. 그러면 배포할 때마다 docker build 입력해야 할텐데 그때마다 속터져 죽습니다.

죽기 싫으면 좋은 방법이 하나 있는데 그게 뭐냐면, Dockerfile 작성시 “빌드할 때마다 변동사항이 많이 생기는 부분들을 최대한 아래 쪽에 적기”입니다. 그럼 build 시간이 단축될 수 있습니다.

💡 Q. 밑에 적는걸로 뭐가 빨라짐?

원래 빌드 작업할 때 COPY, RUN 명령을 실행할 때마다 도커가 몰래 캐싱을 해놓습니다. 캐싱은 “결과를 몰래 저장해놓고, 나중에 필요해지면 재사용한다”는 소리입니다.

  • 캐싱된 명령어들은 매우 빠르게 처리해줄 수 있습니다.
  • 변동사항이 생긴 명령어부터는 캐싱된걸 사용하지 않습니다.
  • 그럼 변동사항이 많은건 좀 아래쪽으로 내리면 좋겠군요.

e.g. Node.js로 웹서버 개발하는 경우는

  • package.json 내용이나 npm install로 라이브러리 설치하는건 날마다 변동사항이 거의 없습니다.
  • (라이브러리를 매일매일 설치하진 않지 않습니까)

그래서

  1. OS와 Node.js 설치하고
  2. package.json 먼저 옮겨서 라이브러리 설치부터 먼저 하고
  3. 그 다음에 자주 변동되는 소스코드 옮기고

그런 식으로 Dockerfile을 작성하면, 매번 docker build 할 때 약간이라도 더 빨라질 수 있는 것입니다.

1
FROM node:20-slim
2
WORKDIR /app
3
COPY package*.json .
4
RUN ["npm", "install"]
5
6
COPY . .
7
EXPOSE 8080
8
CMD ["node", "server.js"]

그래서 이렇게 고쳐봤습니다. 이제 빌드할 때 마다 뭔가 빨라진 느낌이 들 수 있는데 지금은 별차이 없을듯요.


2. npm ci

Dockerfile 작성시 좋은 관습을 몇개 알아봅시다. Node.js 개발할 때 라이브러리 정확한 버전 설치하려면, npm install 말고 npm ci라는 커맨드를 쓰는 것도 좋습니다.

💡 ci는 clean install의 약자임

  • 그냥 npm install하면, package.json에 기록된게 설치되긴 하는데,
    • "express" : "^4.21" 가끔 이런 식으로 표기되어있으면, 맨 앞자리가 4만 되면 된다는 뜻이라서,
    • 나중에 라이브러리가 업데이트되면 실수로 4.22 버전이 막 설치되고 그럴 수 있습니다.
  • 그래서 이거 ^ 표시를 지우거나,
    • 아니면 package-lock.json에 내가 쓰는 라이브러리의 정확한 버전이 써있기 때문에
    • 그걸 바탕으로 설치하라고 입력하는게 npm ci입니다.
    • 심심하면 Dockerfile을 그렇게 수정해봅시다.
1
FROM node:20-slim
2
WORKDIR /app
3
COPY package*.json .
4
RUN ["npm", "ci"]
5
6
COPY . .
7
EXPOSE 8080
8
CMD ["node", "server.js"]

3. ENV

1
# ENV 환경변수이름=값
2
ENV NODE_ENV=production
3
CMD 어쩌구~

ENV 라는 명령어를 쓰면 환경변수를 집어넣어서 이미지를 빌드할 수 있습니다. ENV 환경변수이름=값 사용하면 됩니다.

이런걸 왜 쓰냐면 옛날부터 존재하던 express 같은 라이브러리들은 NODE_ENV=production을 집어 넣어놔야, 로그 출력 양을 좀 줄이고 그래서 성능이 향상되고 그런 케이스가 있습니다.

  • 그래서 Node.js 개발시 설정해두면 나쁠건 없습니다.
  • cf. docker run할 때도 -e 옵션으로 환경변수를 그때그때 집어넣어서 이미지를 실행할 수 있습니다.
    • docker run -e 환경변수이름=‘값’

4. 권한 낮추기

보안적으로 더 나은 습관도 있는데, 원래 Dockerfile에 적은 명령어들은 전부 root 권한으로 실행됩니다. 마지막에 서버 띄우는 명령어는 root 말고 권한을 좀 낮춰서 실행하는게 약간 더 안전하고 좋습니다.

그럴려면 유저를 하나 생성하고 그걸로 유저를 바꿔서 실행하라고 코드짜면 되는데, node 공식 이미지의 경우엔 node라는 이름의 유저가 이미 만들어져있습니다. 그래서 그거 써도 되겠습니다.

1
USER node # node라는 유저로 바꾸라는 명령
2
CMD 어쩌구~

USER 유저이름 적으면 그 유저로 변경됩니다. 유저가 제공되지 않는 이미지는 직접 유저만드는 명령어 찾아서 씁시다.

💡 cf. 실은 지금 하는 것들은 친절한 node 공식 이미지 설명서에 다 나와있는 것들이라

1
FROM node:20-slim
2
WORKDIR /app
3
COPY package*.json .
4
RUN ["npm", "ci"]
5
6
ENV NODE_ENV=production
7
8
COPY . .
9
EXPOSE 8080
10
11
USER node
12
CMD ["node", "server.js"]

5. Spring boot 프로젝트의 경우

Spring boot로 만든 웹서버가 있으면 그건 어떻게 이미지로 만드는지 알아봅시다. Spring boot 모르면 그냥 취미로 들어봅시다.

Spring boot 서버를 실행하려면,

  1. 터미널에서 ./gradlew build 입력해서 .jar파일을 만들고,
  2. 터미널에서 java -jar .jar파일경로 입력해서 .jar 파일 실행하면 끝입니다.

매우 간단한 편이라 Dockerfile도 저렇게 작성하면 이미지 생성 끝입니다.

1
FROM amazoncorretto:21.0.4
2
WORKDIR /app
3
COPY . .
4
RUN ./gradlew build
5
CMD ["java", "-jar", ".jar파일경로"]

프로젝트 폴더에 Dockerfile 만들고 이런거 작성하는게 끝입니다.

  • Java 21버전으로 설치했는데 여러분이 쓰던 버전으로 설치하면 되겠습니다.
  • 아마 .jar 파일은 /build/libs 폴더에 생성되어있을 것입니다.

근데 용량을 더 줄이고 싶으면 이런 편법을 써도 되는데, 실은 생성된 .jar 파일만 있으면 서버를 돌릴 수 있기 때문에, 다른 소스코드나 그런건 전혀 필요없습니다. 그래서 .jar 파일 하나만 담은 이미지를 생성해서 그것만 실행하라고, Dockerfile을 작성하면, 이미지 용량을 훨씬 작게 만들 수 있습니다.

  1. 터미널에 ./gradlew build를 입력해서 .jar파일을 만들기
  2. 새로운 이미지를 만들어서, 그 .jar 파일을 새로운 이미지로 옮기기
  3. 명령어로 .jar 파일을 실행하기

이렇게 작성하면 되는 것임.


5.1 multi-stage build

1
FROM amazoncorretto:21.0.4 AS build # AS로 키워드로 이름짓기 가능
2
WORKDIR /app
3
COPY . .
4
RUN ./gradlew build # jar파일 만듬
5
6
# Runtime stage
7
# 기존거 지우고 이 이미지로 새롭게 시작함
8
FROM amazoncorretto:21.0.4 AS runtime
9
WORKDIR /app
10
COPY --from=build /app/build/libs/*.jar /app/server.jar
11
CMD ["java", "-jar", "/app/server.jar"]

실은 Dockerfile에 FROM을 2번 이상 작성할 수 있는데,

  • FROM을 만날 때 마다 위에 있는 작업내역들이 삭제되고 새로운 마음으로 깨끗하게 시작됩니다.
  • 근데 깨끗하게 시작할 때 위의 작업내역에서 만든 파일들을 몰래 훔쳐올 수 있습니다.
  • 이게 비결임

첫째 FROM에선 /app 폴더에서 .jar 파일만 만들어줍니다.

  • 두번째 FROM에선 이전 FROM에서 나온 .jar 파일을 /app/server.jar 경로로 훔쳐오라고 했습니다.
  • --from=build 이러면 build라고 이름지은 곳에 있던 파일을 카피하라는 뜻입니다.
  • (AS 명령어 쓰면 FROM마다 이름을 마음대로 붙일 수 있습니다.)
  • 그 다음에 마지막에 .jar 파일을 실행하는겁니다.

그럼 이제 최종 이미지에는 .jar 파일, 리눅스OS, 자바21 JDK 이 정도만 들어있어서 좀 가벼워졌겠군요.

  • FROM 여러번 쓰는 짓거리를 multi-stage build라고 하는데,
  • 그래서 빌드과정이 필요한 프로젝트들은 이런 식으로 작성해서 용량을 줄이고 보안도 약간 챙길 수 있습니다.

5.2 bootBuildImage 명령

실은 Spring boot에서 gradle을 쓰는 경우에는 이미지 만드는 명령어가 아마 내장되어있습니다.

  • 터미널에 ./gradlew bootBuildImage 입력하면 이미지를 자동으로 만들어주기 때문에,
  • Dockerfile 작성귀찮으면 한 번 사용해봅시다.

6. Next.js 프로젝트는

Next.js 프로젝트도 코드를 다 짰으면 npm run build 명령어 입력하고, npm start 이런 걸로 코드를 실행해야합니다.

  • 그래서 빌드 과정이 필요하기 때문에,
    • 이것도 Dockerfile 작성할 때, multi-stage build 잡기술을 이용하면 용량을 더 줄일 수 있습니다.
  • 근데 그것보다 더 간편한게 있는데, nextjs output standalone 같은거 검색해봅시다.
    • 그러면 배포할 때 꼭 필요한 파일만 알아서 남겨줍니다.

오늘의 결론은 Dockerfile 작성할 때 잡기술 넣으면 여러 장점이 있습니다. 그리고 성능이나 최적화에 집착하면 고수처럼 보일 수 있습니다.