포스트

Docker (4) - Dockerfile 구조

Docker (4) - Dockerfile 구조

1. 개요

Dockerfiledocker build가 image를 만들 때 읽는 레시피이다. FROM, COPY, RUN 같은 명령어를 한 줄씩 조합해서 “어떤 환경 위에 어떤 파일을 올리고, 어떤 명령을 실행할지”를 정의한다.

기본적인 명령어의 의미를 이해하고 나면, FROM을 여러 번 써서 빌드/테스트/배포 단계를 분리하는 멀티 스테이지 빌드까지 자연스럽게 이어진다. 이번 글에서는 Dockerfile의 기본 구조와 멀티 스테이지 빌드를 함께 정리한다.


2. Dockerfile 기본 구조

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

위와 같이 있다면 한 줄씩 이해를 해보자.

FROM

FROM node:18-alpine 은 베이스 이미지를 지정한다. 즉 내 컨테이너를 만들 때 출발 점이 되는 이미지를 지정한다.

“이 환경 위에서 내 앱을 올릴게” 라고 선언하는 것

WORKDIR

WORKDIR /app 컨테이너 내부의 작업 디렉토리를 /app 으로 설정한다. 이후에 나오는 COPY, RUN, CMD 등의 명령어는 모두 이 경로를 기준으로 실행된다. 디렉토리가 없으면 자동으로 생성된다.

COPY

COPY package*.json ./ 호스트(내 컴퓨터)의 package.json과 pacakge-lock.json을 컨테이너의 현재 작업 디렉토리(/app)로 복사한다.

소스 코드 전체보다 의존성 파일을 먼저 복사하는 이유는 Docker의 레이어 캐싱을 활용하기 위해서이다. 소스코드가 바뀌어도 package.json이 변경되지 않았다면 npm install 단계를 다시 실행하지 않아 빌드 속도가 빨라진다.

RUN

RUN npm install 이미지를 빌드하는 시점에 의존성을 설치한다. RUN 명령어는 빌드 중에 실행되며, 실행 결과가 이미지의 새로운 레이어로 저장된다. (CMD는 컨테이너가 실행될 때 실행되는 것과 차이가 있다.)

ENV

ENV 는 컨테이너 내부에서 사용할 환경 변수를 설정한다.

1
2
ENV NODE_ENV=production
ENV PORT=3000

이렇게 지정한 환경 변수는 이후의 RUN, CMD, ENTRYPOINT 등에서 사용할 수 있고, 컨테이너가 실행된 뒤에도 printenv나 애플리케이션 코드(process.env.PORT 등)에서 그대로 읽을 수 있다. 즉 빌드 시점뿐 아니라 컨테이너가 살아있는 동안 계속 유지되는 값이다.

docker run -e KEY=VALUE 로 컨테이너 실행 시 값을 덮어쓸 수 있다. 그래서 Dockerfile의 ENV에는 기본값을 넣어두고, 실제 배포 환경에 따라 달라지는 값(DB 접속 정보 등)은 실행 시점에 -e나 –env-file로 주입하는 방식을 많이 쓴다.

비슷하게 생긴 ARG는 빌드 시점(docker build 할 때)에만 쓸 수 있는 변수이고, 이미지가 만들어진 후 컨테이너에는 남지 않는다는 점에서 ENV와 차이가 있다.

COPY

COPY . . 프로젝트의 나머지 소스 코드 전체를 컨테이너로 복사한다. COPY {src} {dest} 형식

  • 앞의 . 현재 Dockerfile이 있는 디렉토리, 즉 빌드 컨텍스트를 의미한다.
    1
    2
    3
    4
    5
    
    my-project/
    ├── Dockerfile
    ├── src/
    ├── package.json
    └── index.js
    
  • 뒤의 . 컨테이너 안의 현재 작업 디렉토리(/app)를 의미한다.

**<최종 폴더="" 구조="">** ![](https://velog.velcdn.com/images/eta_kyung/post/1c17c3d5-f9ea-4b5b-b8b4-271196d9f025/image.png)

.dockerignore 파일에 node_modules, .env 등을 등록해놓으면 불필요한 파일이 복사되는 것을 방지할 수 있다.

EXPOSE

EXPOSE 3000 컨테이너가 3000번 포트를 사용한다는 것을 문서화하는 역할이다.

실제로 외부 포트를 외부로 열어주는 것은 아니며, docker run -p 3000:3000 명령어로 호스트와 포트를 연결해줘야 한다.

CMD

CMD [“node”, “server.js”] 컨테이너가 시작될 때 실행할 기본 명령어를 지정한다. RUN과 달리 빌드 시점이 아닌 컨테이너 실행 시점에 동작한다.

배열 형태(exec form)로 작성되는 것이 권장되며, docker run 실행 시 명령어를 추가하면 덮어쓸 수 있다.


3. 멀티 스테이지 빌드

Dockerfile은 FROM을 여러 번 써서 하나의 파일 안에 여러 개의 빌드 단계(스테이지)를 정의할 수 있다. 이를 멀티 스테이지 빌드(multi-stage build)라고 한다.

1
2
3
4
5
6
7
8
9
10
FROM diamol/base AS build-stage
RUN echo 'Building...' > /build.txt

FROM diamol/base AS test-stage
COPY --from=build-stage /build.txt /build.txt
RUN echo 'Testing...' > /build.txt

FROM diamol/base
COPY --from=test-stage /build.txt /build.txt
CMD cat /build.txt

스테이지 구조

FROM은 독립된 빌드 단계를 시작하며, AS 이름으로 해당 스테이지에 이름을 붙일 수 있다.

  • build-stage: diamol/base를 기반으로 /build.txt 파일을 생성한다. (빌드 단계 흉내)
  • test-stage: build-stage에서 만든 /build.txtCOPY --from=build-stage로 가져온 뒤, 다시 덮어써서 테스트 결과를 기록한다. (테스트 단계 흉내)
  • 마지막 스테이지(이름 없음): test-stage의 결과 파일만 COPY --from=test-stage로 가져와서, 이 파일을 실행 시점에 출력한다.

COPY –from={스테이지 이름}을 사용하면 이전 스테이지의 결과물만 골라서 다음 스테이지로 가져올 수 있다. 스테이지 이름 대신 인덱스(0, 1, 2 …)를 써도 된다.

왜 쓰는가

마지막에 실제로 이미지에 남는 것은 최종 스테이지의 내용뿐이다. 위 예시에서는 test-stage가 만든 /build.txt 하나만 최종 이미지에 포함되고, build-stagetest-stage에서 사용했던 중간 산출물이나 빌드 도구들은 최종 이미지에 남지 않는다.

예를 들어 컴파일러, 소스 코드, 테스트 프레임워크처럼 빌드/테스트 과정에만 필요하고 실행에는 필요 없는 것들을 최종 이미지에서 빼버릴 수 있어서, 이미지 용량을 크게 줄일 수 있다.

이 방식 덕분에 하나의 Dockerfile로 “빌드 → 테스트 → 배포용 경량 이미지” 흐름을 전부 표현하면서도, 최종 이미지는 실행에 필요한 파일만 담은 깔끔한 상태로 유지할 수 있다.


4. 정리

Dockerfile은 FROM으로 베이스 이미지를 정하고, WORKDIR·COPY·RUN·ENV로 빌드 시점의 환경과 파일을 구성한 뒤, EXPOSE·CMD로 실행 시점의 동작을 정의하는 흐름을 따른다. 각 명령어는 빌드 시점(RUN, COPY)과 실행 시점(CMD, 그리고 ENV로 유지되는 값)의 역할이 다르다는 점을 구분해서 이해하면 헷갈리지 않는다.

멀티 스테이지 빌드는 이 Dockerfile 구조를 여러 번 반복해서, 빌드·테스트에 필요한 도구와 중간 산출물은 최종 이미지에서 걷어내고 실행에 필요한 결과물만 남기는 방법이다. COPY --from으로 이전 스테이지의 산출물만 선택적으로 가져올 수 있기 때문에, 하나의 Dockerfile로도 가볍고 깔끔한 배포 이미지를 만들 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.