티스토리 뷰
빌드 스크립트는 단순한 텍스트 파일이 아니다
구글에 "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)을 쓴다. 조금 무겁지만 호환성은 좋다.
코드 몇 줄 안 되지만, 여기엔 캐싱 전략, 용량 최적화, 보안, 프로세스 관리의 노하우가 다 들어있다.
- 베이스 이미지: 구체적인 버전을 명시해라. (node:18-alpine)
- 순서의 미학: 변경이 적은 파일(package.json)부터 먼저 복사하고 설치해라.
- 다이어트: .dockerignore는 선택이 아닌 필수다.
위 예시는 단일 스테이지 빌드다. 하지만 실제 배포 환경에서는 빌드 도구조차 남기기 싫어서, 빌드하는 과정과 실행하는 과정을 나누는 Multi-stage Build를 쓴다.
끝!
반응형
'Docker' 카테고리의 다른 글
| Docker] 6. 도커 레지스트리와 배포 (0) | 2025.12.05 |
|---|---|
| [Docker] 5. Docker Compose: 컨테이너 오케스트라의 지휘자 (0) | 2025.12.05 |
| [Docker] 4. 이미지 다이어트의 정석: Multi-stage Build (0) | 2025.12.05 |
| [Docker] 2. 설치 (0) | 2025.12.05 |
| [Docker] 1. Docker, 제대로 좀 알고 쓰자 (0) | 2025.12.05 |
Comments
최근에 올라온 글
최근에 달린 댓글
TAG
- react
- Async
- Promise
- 인천 구월동 맛집
- 맛집
- await
- 정보보안기사 #실기 #정리
- javascript
- 이탈리안 레스토랑
- AsyncStorage
- redux
- redux-thunk
- 파니노구스토
- 인천 구월동 이탈리안 맛집
- react-native
- Total
- Today
- Yesterday
