치춘짱베리굿나이스

[Rank 5] inception - Dockerfile 본문

42/42s Cursus

[Rank 5] inception - Dockerfile

치춘 2022. 10. 14. 08:52

Dockerfile

설명

이미지를 빌드하는 데에 사용되는 DSL (도메인 특화 언어) 로, 도커에서의 Makefile 이라고 생각하면 쉽다

도커에서 모든 명령을 docker run [명령] 한 줄 한줄 입력하는 방식으로 이미지를 빌드하는 것은 매우 번거로운 일이다

만약 다른 시스템에서 이미지를 빌드할 일이 생긴다면 명령어를 하나하나 복사-붙여넣기 하는 것은 완전한 노가다에 다름없기 때문에…

 

쉘 스크립트와 같은 파일에 모든 명령을 모아놓는다면, 스크립트 하나만 들고 다녀도 여러 명령어를 스크립트 실행 한 번에 뚝딱 할 수 있기 떄문에 매우 편리한 것과 같은 이치이다

gcc 명령을 하나하나 따로 입력해서 링크하는 것이 번거롭기 떄문에 Makefile을 이용하여 의존성과 빌드 순서를 정의하는 것과 같은 이치이다

용어

도커 이미지

어플리케이션 하나를 실행시킬 때 필요한 프로그램 본체, 라이브러리, 미들웨어, 환경설정 등을 하나의 객체로 만든 것

프로그램 설치와 설정이 모두 완료된 하나의 실행환경을 파일로 만든 것이라고 보면 된다

한 시점에서의 모든 설정값이 고정되어 있기 때문에 (마치 물 분자를 얼려 고정시키듯…) , 값이 변하지 않는 정적인 객체이다

하나의 도커 이미지를 이용하여 여러 개의 컨테이너를 만들 수 있다

도커 컨테이너

이미지를 실행시키면 컨테이너가 된다

이미지에서 설정한 모든 값들과 설치된 프로그램들이 컨테이너에 고스란히 적용되며, 이를 이용하여 우리가 원하는 서비스를 구동할 수 있는 것이다

서버를 배포할 때 nodejs, nestjs, nginx, … 가 모두 설치된 이미지를 만들고, 이 이미지를 실행만 시키면 서버를 돌릴 수 있는 환경이 모두 조성되고, 쉘 스크립트 실행 커맨드를 등록하면 서버 구동까지 완료할 수 있다

 

컨테이너는 이미지 빌드 시에 등록한 ENTRYPOINT 또는 CMD 명령으로 등록된 명령만 수행하고 바로 종료되기 때문에, 만약 while (1) 등으로 루프를 걸어놓아 지속적으로 켜지게 설정하지 않으면 컨테이너가 곧바로 종료된다

컨테이너 각각은 독자적인 메모리 공간을 할당받으며 컨테이너가 종료되면 이 메모리가 전부 초기화된다

따라서 볼륨 등 휘발되지 않는 메모리 영역을 추가로 설정하여 다음 컨테이너 실행 시에도 값을 유지한다

 

컨테이너는 OS 파일 시스템이 있지만 OS 구동에 중요한 커널이 없기 때문에 용량이 가볍다 (커널은 도커가 설치되어 있는 호스트 OS의 커널을 공유한다)

베이스 이미지

모든 이미지는 베이스가 되는 이미지를 가지고, 그 위에 명령을 수행하거나 추가적인 설정을 진행하여 만들어진다

밑그림으로 사자가 그려진 종이 (베이스 이미지) 를 가지고 그 위에 색칠을 하거나 그림을 덧그려 (명령 수행) 새로운 그림이 그려진 종이 (새로운 이미지) 를 만드는 것 (빌드) 과 같은 이치이다

대개 리눅스 배포판 (알파인, 아치, 데비안 등) 을 베이스 이미지로 사용하며, 그 위에 환경변수 설정이나 포트포워딩, 추가 프로그램 설치 등을 진행하여 새로운 이미지를 만든다

 

Dockerfile에서는 FROM 명령으로 베이스 이미지가 무엇인지 지정한다 (인자는 도커허브에서의 이름을 지정한다)

예를 들어, 우분투 20.04 버전을 베이스 이미지로 사용하고 싶다면 FROM ubuntu:20.04 를 도커파일의 레이어 최상단에 명시하면 된다

레이어

이미지를 구성하는 단위이며, 명령 (RUN, FROM 등의 그것) 하나 당 레이어 하나가 생성된다

정확히는 각 명령마다 이미지가 새로 만들어지며, 레이어는 그 이미지간의 차이점이다

깃이 여러 커밋으로 구성되어 있듯이 도커 이미지는 여러 개의 레이어로 구성되어 있으며, 따라서 여러 이미지를 내려받아도 같은 레이어는 중복하여 내려받지 않는다

 

A 이미지를 다운로드한 뒤 B 이미지를 다운로드한다고 가정하자

만약 A 이미지와 B 이미지 모두 f200ac1e10 레이어가 존재할 경우, B 이미지에서는 f200ac1e10 레이어를 한번 더 다운로드하지 않고 스킵함으로서 중복된 레이어를 갖지 않도록 한다

이런 방식으로 도커는 용량을 효율적으로 관리하며, 깃에서 추가된 커밋만 가져오듯이 도커도 변경된 레이어만 가져오는 방식으로 이미지의 버전 관리를 수행한다

dockerfile에서도 명령을 어떻게 구성하는지에 따라 레이어의 개수가 달라질 수 있기 때문에 (= 용량의 증가), 명령을 구성하는 데에도 주의가 필요하다

볼륨

컨테이너를 실행할 때마다 컨테이너가 할당받은 메모리 영역은 휘발성이라 컨테이너가 종료되면 데이터가 전부 날아간다

또한 이미지는 상태가 보존되긴 하지만 불변하는 특성을 가지고 있기 때문에 이미지에 데이터를 저장할 수도 없는 노릇이다

데이터 공유가 통상적으로는 불가능하기 때문에 도커에서는 볼륨을 이용하여 컨테이너간 데이터 공유를 지원한다

볼륨을 사용하는 방법은 총 세 가지가 있다

  • 컨테이너를 구동시킬 때 -v 옵션을 이용하여 호스트 (도커가 설치된 그곳) 의 특정 폴더를 컨테이너가 공유하도록 하는 방법
  • 볼륨 컨테이너를 생성하여 다른 컨테이너들이 --volumes-from 옵션을 이용하여 해당 컨테이너를 참조하도록 하는 방법
  • docker volume을 이용하여 볼륨을 만들고, 이를 연결하는 방법

멀티 스테이지 빌드

이미지를 빌드할 때, Dockerfile 내부에서 필요한 추가 파일들을 컴파일 또는 생성하여 이미지 안에 넣고 싶을 경우 사용한다

예를 들면 이미지 빌드를 위해 gcc를 설치하는 것은 무겁지만 gcc로 컴파일한 a.out 파일은 꼭 필요할 때, 스테이지를 여러 개 두어 각각의 스테이지에서 베이스 이미지를 따로따로 지정하여 필요한 파일들을 컴파일하고 최종 빌드 시에 a.out만 가져다 사용할 수 있다

 

##################### 스테이지의 시작
FROM gcc:4.9 AS cfile 
# FROM [이미지이름] AS [스테이지 별칭]

#
# 명령 수행
#

##################### 스테이지의 끝

##################### 스테이지의 시작
FROM node:16-alpine AS nodefile 

#
# 명령 수행
#

######################스테이지의 끝

##################### 스테이지의 시작
FROM ubuntu:20.04

COPY --from=cfile /home/chichoon/a.out ./
COPY --from=nodefile /home/chichoon/output.js ./

# COPY --from=스테이지이름 [해당 스테이지에서의 원본 파일 위치] [파일을 붙여넣을 목적지]

...
######################스테이지의 끝

각 스테이지별로 서로 다른 베이스 이미지를 가지며, AS 키워드를 사용하여 스테이지의 별칭을 지정한다 (최종 이미지 빌드 스테이지는 AS 키워드를 쓰지 않는다)

최종 이미지를 빌드할 때 --from=별칭 옵션을 붙이면 해당 스테이지에서의 결과물 파일을 가져올 수 있다

또한, 각 스테이지별 베이스 이미지는 목적 파일을 생성한 뒤 지워지고, --from= 옵션을 통해 별칭을 붙인 결과물 파일 또한 최종 이미지 빌드가 끝나면 삭제된다

최종 빌드되는 결과물은 이미지 단 하나지만, 이미지를 빌드하는 데에 사용되는 부가 요소들을 빌드하기 위해 단계를 나눈 것이라고 보면 된다

문법

Shell form, Exec form

# exec form
RUN ['[명령어]', '[인자]', '[인자]', ...]

# shell form
RUN [명령어] [인자] [인자] ...  
# RUN ['/bin/bash' '-c' '[명령어]' '[인자]' '[인자]'...] 와 동일

RUN, CMD, ENTRYPOINT 등 특정 쉘 명령어를 수행하도록 지정하는 명령에서는 Shell form 과 Exec form의 두 가지 방법으로 인자를 넣어줄 수 있다

  • Shell form은 쉘 (/bin/sh, /bin/bash 등) 에 -c 옵션을 붙여 명령어를 실행하는 것과 동일하게 동작한다
    • 쉘을 경유하고 싶을 경우 (예시: 등록한 환경변수를 사용할 때) 사용한다
    • 자식 프로세스로 쉘을 실행시키므로 코스트가 크지만, 쉘의 특성인 서브커맨드나 파이프, IO 리디렉션, 체이닝 등을 사용할 수 있어 유용하다
    • RUN 커맨드는 Shell form을 자주 사용한다
  • Exec form은 쉘을 경유하지 않고 직접 실행한다
    • 환경변수를 사용할 수 없다
    • JSON 배열 형식을 따르므로 문자열은 모두 홑따옴표로 감싸져야 한다
    • 쉘을 경유할 경우 자식 프로세스로 쉘을 실행시키는데, 이 때 시그널 전달이 잘 이루어지지 않을 가능성이 있으므로 시그널 전달이 필요한 경우 Exec form을 사용하는 것이 안전하다
    • ENTRYPOINT, CMD 커맨드는 컨테이너 생성과 함께 스크립트를 돌리는 경우가 잦으므로 Exec form을 자주 사용한다

FROM

FROM [이미지 이름]

베이스 이미지를 지정하는 명령

FROM [베이스 이미지 이름] 형식으로 작성한다

FROM문 아랫줄에 적을 모든 명령들 (RUN, ENV, COPY 등) 은 이 베이스 이미지를 기반으로 한 새로운 이미지에 작성되는 것이다

부모 클래스 (베이스 이미지) 를 상속받아 메서드를 더 추가하는 (명령 수행) 것과 같은 이치가 아닐까..? 하는

 

FROM [이미지 이름] AS [스테이지 별칭]

멀티 스테이지 빌드를 진행한다면, AS 키워드를 이용하여 스테이지의 이름 (별칭) 을 지정해줄 수 있다

RUN

# exec form
RUN ['[명령어]', '[인자]', '[인자]', ...]

# shell form
RUN [명령어] [인자] [인자] ...  
# RUN ['/bin/bash' '-c' '[명령어]' '[인자]' '[인자]'...] 와 동일

echols 등의 쉘 명령어 (빌트인, 또는 그외 프로그램) 를 실행시키며, Dockerfile을 작성할 때 가장 많이 사용하는 명령어나 다름없다

Shell form 또는 Exec form 중 하나를 사용할 수 있다

대개 && 연산 또는 파이프를 이용하여 RUN 명령의 개수를 최소한으로 줄이기 위해 (= 레이어의 개수를 줄이기 위해) Shell form을 차용한다

공식 Dockerfile들 (nginx, wordpress 등) 을 열어보면 && 커맨드 도배를 통해 필사적으로 RUN 커맨드 개수를 줄이려 노력한 것을 볼 수 있다…

ENV

ENV [환경변수 키] [환경변수 값]

이미지 내에서의 환경변수를 지정하는 명령이다

띄어쓰기를 기준으로 키-값을 입력할 경우 단 하나의 환경변수 쌍만 만들 수 있으며, 하나의 명령 (ENV, RUN) 당 하나의 레이어가 생성되므로 엄청나게 많은 레이어가 생성된다

따라서 환경변수가 하나밖에 없는 것이 아니라면 권장하지 않는다고 한다

 

ENV [환경변수 키]=[환경변수 값] \
        [환경변수 키]=[환경변수 값] \
        [환경변수 키]=[환경변수 값]

= 기호를 기준으로 키-값을 입력할 경우 띄어쓰기 (또는 역슬래시 = 이스케이프) 를 기준으로 여러 개의 환경변수를 설정할 수 있게 된다

 

docker container run [컨테이너명] --env [환경변수 키]=[환경변수 값]

컨테이너 실행 시에 환경변수 값을 바꾸거나 등록하여 실행하고 싶다면, 위의 옵션을 사용한다

WORKDIR

WORKDIR [이동할 경로]

chdir 또는 cd와 비슷한 역할을 하며, 절대경로와 상대경로를 모두 지원한다

CMD, ENTRYPOINT, COPY 등의 명령이 실행되는 기준 디렉토리 (current working directory) 를 바꿀 수 있다

쉽게 말해, . 이 가리키는 경로가 바뀐다고 생각하면 쉽다

COPY

COPY [호스트 파일 시스템의 파일 경로] [도커 이미지의 파일 시스템상 목적지 경로]

호스트가 가지고 있는 파일을 도커 이미지 (컨테이너) 상의 파일 시스템에 복사하는 작업이다

상대경로와 절대경로를 모두 지원하며, 도커 이미지 쪽에서의 상대경로는 WORKDIR 명령을 통해 설정한 경로를 기준으로 적용된다

 

COPY . .

예시로, 호스트의 현재 디렉토리 (dockerfile이 위치한 폴더) 내의 모든 내용물을 도커 컨테이너의 현재 경로로 전부 복사하는 명령은 위와 같다

ADD

ADD [호스트 파일 시스템의 파일 경로 또는 데이터 다운로드 URL] [도커 이미지의 파일 시스템상 목적지 경로]

COPY의 강화판이라 볼 수 있다

ADDCOPY에 2가지 기능을 추가한 명령인데,

  • URL을 인자로 받아 로컬 (호스트 쪽 파일 시스템) 에 존재하지 않는 (네트워크상에 존재하는) 파일도 다운로드하여 컨테이너 쪽에 저장할 수 있다
  • 압축 파일 (tar.xz 등) 인자로 받았다면 자동으로 압축 해제 (추출) 하여 저장한다

단순 파일 복사는 COPY, 위와 같이 특수한 경우에는 ADD를 사용하는 것이 좋다

ENTRYPOINT

# exec form
ENTRYPOINT ['[명령어]', '[인자]', '[인자]', ...]

# shell form
ENTRYPOINT [명령어] [인자] [인자] ...  

이 이미지 파일을 실행시켜 컨테이너로 띄워줄 때, 가장 처음으로 수행해야 하는 명령을 지정한다

RUN과 마찬가지로 Shell form과 Exec form 중 선택할 수 있으나, 자식 프로세스에서의 시그널 씹힘 이슈로 대개 Exec form을 사용한다

RUN은 이미지를 빌드할 때 수행되는 명령, ENTRYPOINT는 빌드된 이미지를 실행시킬 때 수행되는 명령이라는 차이가 있다 (중요)

엔트리 포인트는 Dockerfile에서 단 한 개만 존재 가능하다 (한 컨테이너 당 하나의 서비스를 수행할 수 있다는 원칙)

 

ENTRYPOINT [’sleep’, ‘1000’]

예를 들어, 위 명령은 컨테이너가 생성되면 1초를 대기한다

만약 ENTRYPOINT (또는 CMD) 를 지정하지 않으면, 컨테이너는 생성되자마자 아무런 일도 수행하지 않고 종료된다

또한 엔트리 포인트로 설정한 명령이 실행 도중 죽어버리면, 컨테이너도 같이 종료된다

보통은 여러 일을 수행하거나 컨테이너를 살아 있도록 유지해 주는 쉘 스크립트를 작성하여 컨테이너에 넣어준 후, 해당 스크립트를 실행시키는 엔트리포인트를 추가하는 편이다

CMD

# exec form
CMD ['[명령어]', '[인자]', '[인자]', ...]

# ENTRYPOINT와 함께 사용하는 경우
CMD ['[인자]', '[인자]', ...]

# shell form
CMD [명령어] [인자] [인자] ...  

ENTRYPOINT와 유사하나, 컨테이너 실행 시 옵션을 지정하는 것으로 CMD에 설정한 값을 덮어씌울 수 있다

한 마디로, CMD는 컨테이너를 실행할 때 기본값으로 수행되는 명령을 지정해주는 역할로, 이는 실행 시마다 얼마든지 바뀔 수 있다는 뜻이다

 

ENTRYPOINT ['sleep']

CMD ['1000']

CMDENTRYPOINT는 함께 사용할 수 있으며, ENTRYPOINT는 명령어만 등록하고 CMD로 기본 옵션 (인자) 을 지정해주는 식으로 사용할 수 있다

 

$> docker run testimage 10000

위의 경우,

  1. 기본값 인자로 1000을 지정하였다
  2. 이미지 실행 시 인자로 10000을 넘겨주었으므로
  3. CMD로 지정해준 1000은 10000으로 덮어씌워진다

EXPOSE

EXPOSE [포트번호]

EXPOSE [포트번호]/[프로토콜]

포트 또는 프로토콜을 지정해주는 명령이다

프로토콜은 UDP 또는 TCP 중 하나를 사용할 수 있으며, 기본값 (따로 지정해주지 않을 경우) 은 TCP이다

EXPOSE 명령으로 지정한 포트는 컨테이너에서만 유효하므로, 호스트 측에서 접근하고 싶다면 docker run 시의 -p 옵션을 통한 포트포워딩이 필요하다


참고자료

https://hyeo-noo.tistory.com/340

https://spidyweb.tistory.com/278

https://velog.io/@ckstn0777/도커-볼륨

https://stackoverflow.com/questions/30494050/how-do-i-pass-environment-variables-to-docker-containers

https://kimjingo.tistory.com/78

https://emmer.dev/blog/docker-shell-vs.-exec-form/#shell-features

https://parkgaebung.tistory.com/44

https://www.daleseo.com/dockerfile/

https://www.44bits.io/ko/post/building-docker-image-basic-commit-diff-and-dockerfile#컨테이너의-이해-컨테이너마다-고유의-공간을-가진다

'42 > 42s Cursus' 카테고리의 다른 글

[Rank 5] ft_irc (작성중)  (0) 2023.01.07
[Rank 5] inception - Docker-compose  (0) 2022.10.26
[Rank 4] CPP 08  (0) 2022.09.18
[Rank 4] CPP 07  (0) 2022.09.17
[Rank 4] CPP 06  (0) 2022.09.17
Comments