티스토리 뷰

빌드 스크립트는 단순한 텍스트 파일이 아니다

 구글에 "Node.js Dockerfile", "Python Dockerfile"을 검색하면 수천 개의 예제가 쏟아진다. 대부분 그걸 복사해서 프로젝트 루트에 넣고 빌드 버튼을 누른다. 운 좋게 돌아가면 다행이지만, 조금만 커스텀하려고 하면 막막해진다.

 

"왜 내 이미지는 1GB가 넘지?" "왜 코드 한 줄 고쳤는데 빌드가 5분이나 걸리지?"

 

이 질문의 답은 Dockerfile 안에 있다.

 

Dockerfile은 이미지를 만드는 설계도이자, 서버 구성을 자동화하는 스크립트다.

 

이 설계도의 문법과 순서를 이해해야 효율적인 이미지를 만들 수 있다.

 

가장 대중적인 Node.js 애플리케이션을 예시로 작성법을 해부한다.


Dockerfile 문법 해부 (Core Knowledge)

Dockerfile은 기본적으로 명령어 + 인자 형태로 구성된다.

 

위에서부터 아래로 순차적으로 실행되며, 각 명령어는 새로운 레이어(Layer)를 만든다.

  • Dockerfile 라인별 해부 (Deep Dive) 이제 위 코드를 한 줄씩 뜯어보며, 그 속에 숨겨진 의도를 보자.
  • # [1] Base Image: 가볍고 안정적인 버전 선택
    FROM node:18-alpine
    
    # [2] 환경 변수 설정 (선택 사항)
    ENV NODE_ENV=production
    
    # [3] 작업 디렉토리 생성 및 이동
    WORKDIR /app
    
    # [4] 의존성 파일 우선 복사 (캐싱 전략의 핵심)
    COPY package*.json ./
    
    # [5] 의존성 설치 (Clean Install)
    RUN npm ci --only=production && npm cache clean --force
    
    # [6] 보안 설정: Root 권한 내려놓기
    USER node
    
    # [7] 소스 코드 복사
    COPY --chown=node:node . .
    
    # [8] 포트 문서화
    EXPOSE 3000
    
    # [9] 컨테이너 실행 명령
    CMD ["node", "app.js"]
    

 1. FROM node:18-alpine

  • node:18: LTS(Long Term Support) 버전을 사용한다. 최신 기능보다는 안정성이 중요하다.
  • alpine: 알파인 리눅스는 초경량판이다. 일반 버전(Debian 기반)이 수백 MB라면, 알파인은 50MB 수준이다. 이미지 크기가 작으면 배포 속도가 빨라지고 비용이 준다.

2. 작업 디렉토리

  • 루트(/)에 파일을 풀지 마라. 나중에 시스템 폴더랑 섞여서 관리가 안 된다. /app이라는 별도 공간을 만들어서 격리하는 게 국룰이다.

3. 의존성 복사 (가장 중요)

  • 소스 코드(COPY . .)보다 이걸 먼저 했다는 게 포인트다.
  • 도커는 위에서부터 레이어를 쌓는다. 소스 코드를 고쳐도 package.json이 안 바뀌었다면, 도커는 이 단계까지 캐시(Cache)를 쓴다.
  • 즉, npm install 과정을 건너뛴다는 소리다. 빌드 시간이 1분 걸릴 게 1초로 줄어든다.

4. 의존성 설치

  • npm install 대신 npm ci를 썼다. package-lock.json을 기준으로 엄격하게 버전을 맞춰 설치한다. 협업 시 버전 충돌을 막아준다.
  • --only=production: 개발용 라이브러리(devDependencies)는 설치하지 않는다. 이미지 가벼워지는 소리 들리지 않나?
  • npm cache clean: 설치 후 남은 찌꺼기 파일을 지워서 용량을 더 줄인다.

5. 보안 설정 (User)

  • 이거 안 쓰는 사람 많다. 기본적으로 도커 컨테이너는 root 권한으로 실행된다. 만약 해커가 앱을 뚫으면, root 권한까지 탈취당할 수 있다는 뜻이다.
  • Node.js 이미지는 node라는 기본 유저를 제공한다. 이걸 사용해서 권한을 제한해야한다. 보안의 기본이다.

6. 소스 코드 복사

  • 이제야 소스 코드를 복사한다.
  • --chown=node:node: 위에서 유저를 node로 바꿨으니, 복사하는 파일의 주인도 node로 맞춰준다. 안 그러면 권한 에러(Permission Denied)로 고생한다.

7. 실행 명령

  • node app.js라고 그냥 적지 말고, 대괄호[]를 써야한다(Exec form).
  • 그냥 적으면 쉘(/bin/sh)이 감싸서 실행되는데, 이러면 나중에 컨테이너를 종료하라는 신호(SIGTERM)를 앱이 못 받는다. 즉, 강제 종료되어 데이터가 깨질 수 있다. 대괄호를 써야 신호를 제대로 받아서 안전하게 종료(Graceful Shutdown)된다.

 

삽질 로그 (Troubleshooting)

작성법은 간단해 보이지만, 실제 빌드 과정에서 자주 겪는 문제들이다.

[Case 1] 불필요한 파일까지 복사됨 (Context 과부하)

  • 상황: 분명 소스 코드는 몇 MB 안 되는데, 빌드 컨텍스트 전송 시간이 엄청 오래 걸리고 이미지 크기도 기형적으로 크다.
  • 원인 분석: COPY . . 명령어 때문이다. 내 로컬에 있는 node_modules(이미 설치된 거대 폴더), .git 폴더, 맥북의 .DS_Store 같은 쓰레기 파일까지 몽땅 컨테이너로 복사했기 때문이다. 로컬의 node_modules는 OS가 달라서 컨테이너에서 돌아가지도 않는다.
  • 해결책: .dockerignore 파일을 프로젝트 루트에 만든다. .gitignore와 문법이 같다. 여기에 제외할 파일들을 적어준다.
    # .dockerignore
    node_modules
    .git
    .env
    Dockerfile
    

[Case 2] 윈도우 줄바꿈 문자 문제 (CRLF)

  • 상황/에러메시지: 윈도우에서 작성한 쉘 스크립트(.sh)를 COPY해서 실행하려는데 not found 혹은 이상한 문자가 섞여서 에러가 난다.
  • 원인 분석: 앞선 글에서도 언급했지만, 윈도우는 줄바꿈을 CRLF(\r\n)로 하고 리눅스는 LF(\n)로 한다. 도커 컨테이너는 리눅스다. 쉘 스크립트 해석기가 \r 문자를 명령어의 일부로 인식해서 오작동하는 것이다.
  • 해결책: 에디터 설정에서 줄바꿈 형식을 LF로 바꾸거나, COPY 하기 전에 .gitattributes를 설정해서 깃이 알아서 변환하게 해야 한다. 급하면 Dockerfile 안에서 RUN sed -i 's/\r$//' script.sh 같은 명령어로 변환할 수도 있지만, 근본적인 해결책은 원본 파일을 LF로 저장하는 것이다.

[Case 3] "분명 코드를 고쳤는데 반영이 안 돼요"

  • 상황: app.js를 수정하고 다시 빌드했는데, 컨테이너를 띄워보면 예전 코드가 돌아간다.
  • 원인 분석: 도커파일 문제가 아니다. docker build 할 때 태그(이름)를 똑같이 덮어쓰기만 하고, 실행할 때 예전 이미지를 바라보고 있을 확률이 높다. 혹은 브라우저 캐시 문제일 수도 있다.
  • 해결책: 빌드할 때마다 태그를 다르게 붙여라. (예: myapp:v1, myapp:v2). 실무에서는 깃 커밋 해시값을 태그로 붙여서 구분한다.

[Case 4] "bcrypt 설치하다가 터졌어요"

  • 상황: npm install 단계에서 빨간 에러가 좍 뜬다. 특히 node-gyp, python 어쩌고 하는 에러들.
  • 원인 분석: alpine 이미지는 너무 가벼워서 C++ 컴파일러나 파이썬 같은 빌드 도구가 아예 없다. bcrypt 같은 라이브러리는 설치 과정에서 컴파일이 필요하다.
  • 해결책: 방법 1: RUN apk add --no-cache python3 make g++ 명령어로 필요한 도구를 설치한다. 방법 2: 그냥 alpine 말고 slim 버전(node:18-slim)을 쓴다. 조금 무겁지만 호환성은 좋다.

 

 

코드 몇 줄 안 되지만, 여기엔 캐싱 전략, 용량 최적화, 보안, 프로세스 관리의 노하우가 다 들어있다.

  1. 베이스 이미지: 구체적인 버전을 명시해라. (node:18-alpine)
  2. 순서의 미학: 변경이 적은 파일(package.json)부터 먼저 복사하고 설치해라.
  3. 다이어트: .dockerignore는 선택이 아닌 필수다.

 위 예시는 단일 스테이지 빌드다. 하지만 실제 배포 환경에서는 빌드 도구조차 남기기 싫어서, 빌드하는 과정과 실행하는 과정을 나누는 Multi-stage Build를 쓴다.

 

 

끝!

반응형
Comments