본문 바로가기
{시리즈}/Docker

5. Dockerfile를 사용하여 이미지 생성

by jay2022 2023. 12. 11.

1. Dockerfile 이란?

공식문서의 정의(Dockerfile reference)

Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. This page describes the commands you can use in a Dockerfile.

쉽게 말하면 Dockerfile은 도커 이미지를 생성하기 위한 명령어 모음이다.

  • Dockerfile은 자체적인 명령어를 사용하여 정의한다.
  • 작성된 명령은 절차적으로 실행된다.
  • 정의된 Dockerfile은 docker build ... 명령을 통해 이미지로 생성된다.

1.1. Dockerfile을 사용하여 이미지 빌드하는 방법

docker build [OPTIONS] PATH | URL | -
  • ex) 현재 경로에 도커 파일이 Dockerfile.user 명으로 있는 경우
## -t: 이미지 이름 정의, -f: 사용할 Dockerfile 이름 명시
docker build -t test_images -f Dockerfile.user .
  • ex) 현재 경로에 도커 파일이 Dockerfile 명으로 있는 경우
## 도커파일명이 "Dockerfile"인 경우 -f는 생략 할 수 있다.
docker build -t test_images .
  • ex) 이미지에 태그를 부여하여 생성
# -t <name:tag>
## tag가 생략되는 경우 'latest'로 자동 부여된다.
docker build -t test_images:tag-v2 .

2. FROM 명령어

FROM [--platform=<platform>] <image> [AS <name>]

FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
  • FROM신규로 생성될 이미지의 Base 이미지를 지정하는 역할을 한다.
  • ARG 명령을 제외하고 FROM은 항상 맨 위에 나와야 한다.
  • --platform 옵션은 이미지의 플렛폼을 지정할 때 사용된다.
    • linux/amd64: 인텔 아키텍처인 x86 기반의 리눅스 플렛폼 지정
    • linux/arm64: arm 아키텍처 기반의 리눅스 플렛폼 지정
    • windows/amd64: 인텔 아키텍처인 x86 기반의 윈도우 플렛폼 지정
  • [AS <name>] FROM은 여러개 정의가 가능하며, 이 경우 구분하기 위해 가명을 지원한다.
    • FROM은 다음 FROM이 나오기 전까지 하나의 네임스페이스를 가지는 코드 블록으로 이해해도 괜찮다.
    • 조금 더 비슷한 비유는 Github Action의 JOB 하나가 FROM 하나로 볼 수 있다.

3. RUN 명령어

# 사용법 1. shell 명령 방식으로 전달
RUN command param1 param2

# 사용법 2. exec 실행 형식 *큰따옴표(")만 사용가능
RUN ["executable", "param1", "param2"]
  • RUN 명령은 이미지 빌드에 필요한 스크립트를 명령을 수행하기 위해 사용된다.
  • RUN 명령은 shell에서 명령을 그대로 사용할 수 있다.
    • 사용 가능한 명령은 결국 FROM에 정의된 Base 이미지에 있는 패키지만 가능하다.
  • RUN 명령은 빌드 로그에 진행 상황으로 표시된다.
    • Github Action에서 step과 비슷하게 로그에 진행 상황이 표시된다.
  • RUN 명령이 실행되면 결과는 커밋이 된다.
    • 커밋된 결과는 다음 FROM에서 접근하여 사용이 가능하다.
  • '사용법 2' 경우 $환경변수에 접근 하는 방식의 명령을 사용하는 경우 약간의 차이가 발생한다.
    • RUN echo $HOME 가능
    • RUN [ "echo", "$HOME" ] 불가능
    • RUN [ "sh", "-c", "echo $HOME" ] 가능
    • 또한 exec 실행 방식은 json 배열이다. 때문에 작은따옴표(')는 사용할 수 없다.

예시1 - RUN 단순 사용

  • Dockerfile
# ubuntu 호스트에 이미지가 없다면 자동 설치
FROM ubuntu

# case 1) 단순 사용
RUN echo $HOME
# case 2) 여러줄로 사용
RUN /bin/bash -c 'source $HOME/.bashrc && \
echo $HOME'
# case 3) exec 실행형식 사용
RUN [ "sh", "-c", "echo $HOME" ]
  • docker build -t test_run . 로그 확인
    ex_dockerfile_run_1
    1) [ 1 / 3 ] : FROM ubuntu 명령으로 인해 docker 레지스트리에서 ubuntu:latest 를 내려 받는다.
    2) [ 2 / 4 ], [ 3 / 4 ], [ 4 / 4 ] : RUN 명령의 경우 빌드 로드에 진행 상황이 표시되는 것을 확인 할 수 있다.

예시2 - 이전 FROMRUN에서 수행된 결과를 다음 FROM에서 사용하기

  • Dockerfile.RUN
FROM ubuntu as job1

RUN echo 'echo "echo 1"' > echo.sh
RUN echo 'echo "echo 2"' >> echo.sh

FROM ubuntu as job2

RUN mkdir ./app
# `--from` 옵션으로 job1에서 생성한 echo.sh를 가져와 복사한다.
COPY --from=job1 /echo.sh ./app/echo.sh
RUN sh ./app/echo.sh
  • docker build -t test_run:tag2 -f Dockerfile.RUN . 로그 확인
    ex_dockerfile_run_2
    1) [ job 1 / 3 ] : job1 과 job2는 동일한 이미지를 사용한다.

    • 때문에 [ job2 1 / 3 ] 로그는 출력되지 않는다.
    • CACHED가 붙는 이유는 '예시1)'에서 설치된 레이어를 사용하기 때문이다. (아래 사진으로 추가 설명)
      2) [ job1 2 / 3] ~ [ job2 4 / 4 ] 로그를 보게되면 job1job2는 순차적으로 실행되지 않는다.
    • 하지만 COPY --from=job1 처럼 job1을 의존하는 경우라면 job1 종료후 순차적으로 수행한다.
  • 새로운 가상환경에서 '예시2'를 다시 빌드한 로그 확인

    1) 새로운 가상환경
    2) [job1 1/3]에서 CACHED가 붙지 않고 신규로 설치된다.

4. CMD 명령어

# 사용법 1. shell 명령 방식으로 전달
CMD command param1 param2

# 사용법 2. exec 실행 형식 *큰따옴표(")만 사용가능
CMD ["executable","param1","param2"]

# 사용법 3. ENTRYPOINT 와 같이 사용시
CMD ["param1","param2"]
  • CMD 명령의 주된 목적은 컨테이너가 정상 실행될 때 "기본값(명령)"을 전달(실행)하는 목적으로 사용된다.
  • 여기서 "기본값"이란 2가지를 의미한다.
    1. 실행파일(명령어)을 전달한다.
    2. 인자를 전달한다. 전달된 인자는 ENTRYPOINT명령에서 사용하게 된다.
  • CMD 명령은 Dockerfile에서 1개만 사용할 수 있다.
    • 여러개 사용하는 경우 가장 마지막 항목만 적용된다.

예시1 - CMD 단순 사용

FROM ubuntu as job1

RUN echo 'echo "echo 1"' > echo.sh
RUN echo 'echo "echo 2"' >> echo.sh

FROM ubuntu as job2

RUN mkdir ./app
# job1에서 생성한 echo.sh를 가져와 복사한다.
COPY --from=job1 /echo.sh ./app/echo.sh
# 실행 권한 추가
RUN chmod +x ./app/echo.sh 
# 컨테이너 실행시 수행될 명령
CMD ["sh", "-c", "./app/echo.sh"]
  • docker build -t test_cmd . 로그 확인
    ex_dockerfile_cmd_1

  • docker inspect test_cmd 결과에서 "Cmd" 확인

5. ENTRYPOINT 명령어

*도커를 많이 사용하지 않았다면 ENTRYPOINT없이 CMD를 주로 활용하는 것을 추천합니다.

# 사용법 1) 비권장 사항
ENTRYPOINT command param1 param2

# 사용법 2) 권장 사항  *큰따옴표(")만 사용가능
ENTRYPOINT ["executable", "param1", "param2"]
  • ENTRYPOINT컨테이너 실행시 수행할 "명령"을 지정하는데 사용한다.
  • ENTRYPOINT는 주로 CMD와 같이 사용된다.
    • 주로 ENTRYPOINT에는 실행할 명령을 넣고 CMD에는 파라미터를 정의 방법으로 사용한다.
  • 이러한 방법을 사용하는 큰 이유는 CMD의 경우 쉽게 재정의 할 수 있게 했기 때문이다.
    • ENTRYPOINT를 재정의 하려면 docker run 명령에 --entrypoint 옵션을 사용해야 한다.
    • 반면 CMD의 경우 docker run <image> param1 parma2 이러한 방식으로 재정의가 가능하다.
  • ENTRYPOINT 명령은 Dockerfile에서 1개만 사용할 수 있다.
    • 여러개 사용하는 경우 가장 마지막 항목만 적용된다.

예시1 - ENTRYPOINT 로 실행 환경 지정

1) 빌드 수행: docker build -t node_entry .

FROM node:18.19.0-alpine

# 컨테이너 실행시 실행될 값 - 변경할 수 없음  
ENTRYPOINT ["node"]

# 컨테너 실행시 실행될 값 - 변경 할 수 있음
CMD ["app.js"]

3) docker inspect node_entry 결과에서 "Cmd", "Entrypoint" 확인
ex_dockerfile_entrypoint_1

3) 호스트에 실행시킬 /root/test.js 파일 준비

console.log('Hello world');
console.log(`process.argv:\n\t${process.argv}`);
console.log(`new Date().toString():\n\t${new Date().toString()}`);

4) /root/test.js을 마운트 하고 컨테이너 실행

docker run -d \
--name node_app \
-v $(pwd)/test.js:/app.js \ # 컨테이너에 생성될 파일 app.js로 정의
node_entry
  • 결과
    ex_dockerfile_entrypoint_2

예시2 - run 명령으로 실행시 CMD를 재정의

  • docker run 명령
docker run -d \
--name node_app \
-v $(pwd)/test.js:/test.js \ # 컨테이너에 생성될 파일 test.js로 정의
node_entry \
test.js param1  # <image> 이후에 오는 인자는 CMD를 재정의 한다.
  • 결과
    ex_dockerfile_entrypoint_3

예시3 - run 명령으로 실행시 ENTRYPOINT/bin/sh 로 재정의

  • 컨테이너 실행시 ENTRYPOINT/bin/sh 로 재정의
docker run \
--name node_app \
-v $(pwd)/test.js:/app.js \
--entrypoint "/bin/sh" \ # ENTRYPOINT 재정의: 실행 환경을 쉘로 변경
node_entry \
-c "cat app.js" # CMD 재정의: 쉘 명령인 cat 실행
  • 결과
    ex_dockerfile_entrypoint_4

💡ENTRYPOINT는 재정의가 어렵다

  • docker run --entrypoint ...를 사용한 재정의에는 제약이 많다.
  • docker run --help를 해보면 알지만 --entrypoint 뒤에는 문자열만 가능하다.
  • 인자로 전달된 문자열에 띄어쓰기가 포함되는 경우 다양한 문제가 발생한다.
  • 결론: 재정의가 어렵기 때문에 CMD 재정의 방법이 더 많이 사용된다.

--entrypoint 실패 사례

# 띄어쓰기로 인해 TZ=Asia/Seoul를 폴더로 인식
## ... runc create failed: unable to start container process: exec: "TZ=Asia/Seoul node": stat TZ=Asia/Seoul node: no such file or directory: unknown.
docker run -d \
--name node_app \
-v $(pwd)/test.js:/app.js \
--entrypoint "TZ=Asia/Seoul node" \
node_entry

# 띄어쓰기로 인해 node 실행 파일을 정상적으로 찾지 못함
## ... runc create failed: unable to start container process: exec: "node -e": executable file not found in $PATH: unknown.
docker run -d \
--name node_app \
-v $(pwd)/test.js:/app.js \
--entrypoint "node -e" \
node_entry \
"process.env.TZ='Asia/Seoul'; require('./app.js')" app.js;

# format 에러
## docker: invalid reference format. See 'docker run --help'.
docker run -d \
--name node_app \
-v $(pwd)/test.js:/app.js \
--entrypoint ["TZ=Asia/Seoul", "node"] \
node_entry

6. ENV 명령어

ENV <key>=<value> ...
  • ENV 명령은 컨테이너가 사용할 환경 변수를 정의하는 명령이다.
  • ENV 사용시 주의사항
    • ENV로 지정된 환경변수의 지속성에 대해 이해하고 사용해야 한다.
    • 잘못된 ENV 사용으로 이전 레이어 또는 다음 레이어의 환경변수를 오염 시킬 수 있다.
    • 참고: Docker docs | commandline | builder#env
  • docker inspect 명령을 통해 생성된 이미지에 정의된 ENV 목록을 볼 수 있다.
  • docker run --env <key>=<value> 을 사용하여 ENV 재정의가 가능하다.
    • 여러 환경변수를 주입하려면 --env <key>=<value>를 연속하여 사용한다.
    • --env <key>=<value>를 사용하면 ENV로 지정되지 않는 환경 변수도 주입이 가능하다.
  • 여러 FROM를 사용하는 경우 최종 FROM에 정의된 ENV 최종적으로 적용된다.
    • 생성된 컨테이너에 환경변수로 사용
    • docker inspect 에서 확인가능

예시1) 이미지 ENV 단순 사용

FROM ubuntu as job1

ENV VAR1="test 1"
ENV VAR2=test\ 2

FROM ubuntu as job2
# 생성된 이미지 마지막 FROM에 정의된 ENV만 적용된다.
ENV VAR3=test3
CMD ["sh", "-c", "echo $VAR1, $VAR2, $VAR3 && env"]
  • docker build -t test_env . 로그 확인
    ex_dockerfile_env_1

  • docker inspect test_env 결과에서 "Env" 확인
    ex_dockerfile_env_2

  • docker run 실행 결과 확인
    ex_dockerfile_env_3

예시2) run 명령으로 실행시 환경변수 재정의

docker run --name env_container \ 
--env VAR3=custom3 \ 
test_env
  • 결과 확인
    ex_dockerfile_env_4

예시3) run 명령으로 실행시 환경변수 신규 정의

docker run --name env_container \
--env CUSTOM=value1 \
--env VAR1=value1 \
test_env
  • 결과 확인
    ex_dockerfile_env_5

7. WORKDIR 명령어

WORKDIR /path/to/workdir
  • WORKDIR 는 단순하게 작업할 공간인 디렉토리를 생성하는 명령이다.
  • WORKDIR 는 쉽게 보면 2가지 명령을 수행한다.
    1. mkdir /path/to/workdir
    2. cd /path/to/workdir
  • WORKDIR 명령은 여러번 사용이 가능하다.
  • 작업 디렉토리를 사용하지 않으면 기본은 루트(/)이다.
  • 작업 디렉토리 설정은 하는 것을 권장한다.
    • 의존하는 이미지에 의해 작업 디렉토리가 설정될 수 있기 때문이라고 한다.

예시1) WORKDIR 공식 예제

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd 
  • pwd의 결과는 /a/b/c 이다.

8. COPY 명령어

# 사용법 1.
COPY <src>... <dest>

# 사용법 2. path에 공백이 있는 경우 사용한다.
COPY ["<src>",... "<dest>"]

# 사용법 3. 권한 부여 사용
COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>

# 사용법 4. 이전 from 결과를 복사
COPY ...권한 [--from=<from>] <src>... <dest>
  • COPY호스트에 있는 파일(디렉토리)을 복사하기 위해 사용한다.
  • 복사된 파일은 빌드시 또는 컨테이너에서 사용된다.
  • <src> 의 경우 히든카드를 통한 찾기도 지원한다.
    • ex) COPY hom* /mydir/
    • ex) COPY hom?.txt /mydir/
  • 일부 파일의 경우 권한이 필요할 수 있기 때문에 권한을 컨트롤하는 --chown 옵션을 제공한다.
  • 또한 --from 옵션을 통해 이전 FROM에서 생성된 파일을 복사하여 사용도 가능하다.

9. 실제 사용 예시로 보는 Dockerfile

9.1. node 환경에서 이미지 빌드 - 기초

FROM node:18.19.0-alpine

# 1. 작업 디렉토리 설정 = 'mkdir /usr/app && cd /usr/app'
WORKDIR /usr/app

# 2. 의존성 라이브러리 다운로드를 위해 복사
COPY package*.json ./ 
# 3. 라이브러리 다운로드 수행
RUN npm ci

# 4. ts 설정 파일 복사
COPY tsconfig.json ./
# 5. 소스코드 포함한 모든 파일 복사
## 이 경우 '.dockerignore' 필수로 사용한다.
COPY . /usr/app

# 6. 빌드 명령 수행
RUN npm run build
# 7. 컨테이너 정상 실행 후 빌드된 결과로 app이 실행되도록 명령어 지정
CMD ["npm","run","start"]
  • 위의 Dockerfile는 보완점이 존재한다.
  • node 실행시 필요 없는 환경을 제거하지 못했기 때문이다.
  • 즉, typescript 환경이나 devDependencies를 제거한 이미지가 되어야 한다.
  • 그렇게 되면 더 경량화된 이미지와 컨테이너를 만들 수 있다.

9.2. node(nestjs) 환경에서 이미지 빌드 - 고급

### builder: 해당 FROM은 빌드만 수행한다. ###
FROM node:18.19.0-alpine as builder
# builder-1) 작업 디렉토리 지정
WORKDIR /app
# builder-2) 의존성 설치
COPY ./package.json /app
COPY ./package-lock.json /app
RUN npm ci
## 다른 방법도 가능하지만 경량화 작업을 수행하기 때문에 이것도 문제가 없다.
RUN npm i -g @nestjs/cli
# builder-3) node_modules에서 devDependencies 제거 
RUN npm prune --production

# builder-4) 빌드 수행
COPY . .
RUN npm run build

### images: 해당 FROM의 결과를 최종 이미지로 생성한다. ###
FROM node:18.19.0-alpine as images
# images-1) 작업 디렉토리 지정
WORKDIR /app
# images-2) 이전 FROM인 'builder'에서 생성된 빌드된 폴더 복사
COPY --from=builder /app/dist /app/dist
# images-3) node_modules 복사
COPY --from=builder /app/node_modules /app/node_modules
# images-4) 컨테이너 정상 실행 후 빌드된 결과로 app이 실행되도록 명령어 지정
CMD ["node", "dist/main"]
  • 해당 이미지의 최종 결과는 앱을 켤 수 있는 상태로 충분하다.
    • /app/dist
    • devDependencies가 제거된 /app/node_modules

경량화 작업을 수행 비교 결과

ex_dockerfile_test_1

  • latest: 'images' 단계 없이 'builder'에서 CMD 명령을 사용한 이미지로 최종 크기가 '348MB' 이다.
  • slim: 'images' 단계를 사용통해 경량화를 수행한 이미지로 최종 크기가 '197MB' 이다.

10. 정리

Dockerfile는 Docker Image를 생성하기 위한 명령을 담은 파일이다.
정의된 Dockerfiledocker build CLI에 파라미터로 전달되어 이미지로 생성된다.

Dockerfile의 주된 명령들을 간단하게 정의하면 아래와 같다.

  • FROM - 신규로 생성될 이미지의 Base 이미지를 지정
  • RUN - 이미지 빌드에 필요한 스크립트를 명령을 수행
  • CMD - 컨테이너가 정상 실행될 때 "기본값(명령)"을 전달(실행)하는 목적
  • ENTRYPOINT - 컨테이너 실행시 수행할 "명령"을 지정
  • ENV - 컨테이너가 사용할 환경 변수를 정의
  • WORKDIR - 작업할 공간인 디렉토리를 생성
  • COPY - 호스트에 있는 파일(디렉토리)을 복사

Dockerfile은 쉬우면서도 제대로 사용하기는 어렵다.
도커에 익숙하지 않은 사람들은 누군가의 Dockerfile을 복사하여 사용할 것이다.
필요에 따라 조금에 수정했는데 에러를 만나는 사람도 분명 있을 것이라고 생각한다.
게다가 빌드에 성공 이후 run 과정에서 에러가 발생하는 경우도 많다.

처음에 내가 그랬다 이해하지 않고 될 때까지 몇 번이고 빌드를 돌려가며 헤딩했다.
나와 같은 누군가 내가 정리한 글을 읽고 도움이 되었으면 좋겠다는 마음이다.
그래서 이번 장은 조금 더 신경써 정리하였다.

참고