Docker 이미지는 그냥 만들어지지 않는다. Dockerfile이라는 명세서에 적힌 명령어를 한 줄씩 실행하며 레이어를 쌓아 올린다. 레이어 하나하나가 캐시되기 때문에 순서와 구성이 빌드 속도에 직접적인 영향을 준다.
FROM ubuntu:22.04 ← 레이어 1 (베이스 이미지)
RUN apt-get update ← 레이어 2
RUN apt-get install -y ... ← 레이어 3
COPY . /app ← 레이어 4
CMD ["node", "index.js"] ← 메타데이터 (레이어 아님)
위에서 아래로 순서대로 실행되며, 변경된 줄 이후의 캐시는 모두 무효화된다.
| 명령어 | 역할 |
|---|---|
FROM | 베이스 이미지 지정 |
RUN | 빌드 시점에 쉘 명령 실행 |
COPY | 로컬 파일을 이미지로 복사 |
ADD | COPY + URL 다운로드 + tar 자동 압축 해제 |
WORKDIR | 이후 명령의 작업 디렉터리 설정 |
ENV | 환경 변수 설정 (빌드 및 런타임 모두) |
EXPOSE | 컨테이너가 사용할 포트 문서화 |
ARG | 빌드 시점에만 유효한 변수 |
CMD | 컨테이너 기본 실행 명령 (오버라이드 가능) |
ENTRYPOINT | 컨테이너 진입점 (오버라이드 어려움) |
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
package*.json을 먼저 복사하고 npm ci를 실행하는 이유가 있다. 소스 코드가 바뀌어도 의존성 레이어 캐시가 유지되어 재빌드가 빠르다.
alpine — 경량 Linux 배포판. 이미지 용량을 크게 줄여준다.
ENTRYPOINT ["node"] # 고정 진입점
CMD ["server.js"] # 기본 인자 (docker run 시 덮어쓸 수 있음)
docker run myapp index.js처럼 실행하면 CMD의 server.js가 index.js로 교체된다. ENTRYPOINT는 유지된다. 두 가지를 함께 쓸 때는 모두 JSON 배열 형식으로 통일해야 한다.
exec form — ["node", "server.js"] 형태. 쉘을 거치지 않아 신호 처리가 정확하다.
ARG BUILD_VERSION=1.0.0
ENV APP_VERSION=$BUILD_VERSION
ARG는 docker build --build-arg BUILD_VERSION=2.0.0으로 주입하며 빌드가 끝나면 사라진다. ENV는 런타임까지 컨테이너 내부에 남는다.
일반적으로 COPY를 사용한다. ADD는 원격 URL 다운로드나 .tar.gz 자동 압축 해제가 필요할 때만 쓴다. 그 외 상황에서 ADD를 쓰면 의도치 않은 동작이 생길 수 있다.
# 권장
COPY ./dist /app/dist
# tar 자동 해제가 필요할 때만
ADD archive.tar.gz /app/
Dockerfile은 인프라를 코드로 관리하는 첫 번째 단계다. 명령어 순서를 캐시 효율 기준으로 정렬하고, 불필요한 레이어를 줄이는 것만으로도 빌드 파이프라인이 눈에 띄게 빨라진다.