[Docker] Dockerfile을 알아보자.

[Docker] Dockerfile을 알아보자.

July 13, 2022

😡 형냐들.. 나만 불편해?

앞서서 Docker의 CLI를 잠깐 맛보았다면, 컨테이너를 생성하고, 만드는 일이 굉장히 불편한 일이라는 것을 알 수 있다.

나는 편하게 하고 싶어서 컨테이너를 만들때마다 수많은 커맨드를 입력해야한다.

사실 그것보다 더 큰 문제가 있다면, 이미지로 빌드하는 순간, 용량이 무지막지하게 커질 수 있다는 사실이다.

엄청 크진 않겠지만, 적어도 100MB부터, 1~2GB가 넘어가는 것까지 다양하다.

img.png

수많은 컨테이너를 관리하는 회사라면, 이런 사항은 더욱 재앙처럼 다가올 수 있다.

용량이야 그렇다 쳐도, 이미지를 전달하고, 이걸 내려받는데 걸리는 시간만 굉장히 오랜 시간이 걸리게 될 것이다.

📄 Dockerfile

그래서 Docker에서는 새롭게 컨테이너를 만드는 방식을 제안한다.

이미 만들어진 이미지가 아니라, 이미지를 만드는 스크립트를 공유하도록 하는 것이 골자이다.

🚫 Don’t Repeat Yourself

어차피 한번은 짜야한다. 이걸 자동화하는 것은 향후 AI가 하려나..?

근데 한번만 짜면 된다.

이는 Docker의 내부 기능이기 때문에 명령어를 짜서 텍스트 파일로 저장하는 것보다 훨씬 간단하며, 형식적이고, 구조적이다.

또한 결론적으로 Dockerfile도 결국 텍스트 파일이기 때문에, 보관 시 용량을 크게 잡아먹지 않는다.

잡설이 길었다. 이제 본론이다.

그럼 어떻게 실행시켜?

Dockerfile을 만들면, 파일의 명세대로 이미지를 불러와, 명령을 수행한다.

컨테이너가 Docker에 띄워지면, 해당 이미지는 사용자의 로컬에 저장된다.

해당 과정을 build라고 부르며, 컨테이너를 이미지로 만드는 commit와는 차이가 있다.

build [BUILD_PATH]

build 명령어는 명시한 경로의 하위에 있는 Dockerfile을 기반하여 컨테이너를 만드는 명령어이다.

이는 해당 명령어는 재귀적으로 수행하기 때문에, 절대 루트 디렉토리를 경로로 삼아선 안된다.

$ docker build / # (X)

일반적으로 프로젝트 루트 디렉토리에 Dockerfile을 위치시키기 때문에, 아래의 명령어가 관용구처럼 활용된다.

$ docker build .

-f [DOCKER_FILE_PATH] 옵션

전통적으로 Dockerfile은 빌드할 프로젝트 루트 경로에 위치하나 -f 옵션을 통해 루트 경로와 Dockerfile의 경로가 달라도 이를 연결시켜줄 수 있다.

$ docker build -f /path/to/a/Dockerfile .

이는 Dockerfile이 마치 현재 디렉토리(.)에 있는 것처럼 행동하도록 한다.

-t 옵션

빌드가 성공하면 새 이미지를 저장할 리포지토리와 태그를 -t 옵션을 통해 지정할 수 있다.

$ docker build -t shykes/myapp .

여러 저장소에 같은 이미지를 두거나, 여러 태그를 저장해야 한다면, 여러 개의 -t를 지정하여 같은 이미지로 빌드할 수 있다.

$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

Dockerfile 만들기

Dockerfile을 만드는 방식은 간단하다. Dockerfile을 만들면 된다.

이제 Dockerfile을 작성하기 위해 방대한 설정을 알아보자

ARG

Dockerfile 내에서 변수의 재 사용성을 극대화시키기 위해 사용하는 변수 선언문이다.

# Usage of Definition
ARG [KEY]=[value]

# Usage of Call
${[KEY]}

# Example
ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD  /code/run-app

FROM extras:${CODE_VERSION}
CMD  /code/run-extras

FROM

# - Usage
FROM [--platform=<platform>] <image> [AS <name>]
# or
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
# or 
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]

작업할 기반 이미지를 표기한다.

이전 챕터의 run, create 명령어에서 살펴보았던 IMAGE 부분에 적을 내용을 기입한다고 보면 되며 해당 부분에 적힌 이미지는 먼저 로컬에서 찾아보고, 찾은 후 없다면, Docker Hub 에서 이미지를 찾아 가져온다.

물론 접근 권한이 없는 Repository에 접근하거나, 이미지 명을 잘못 적은 경우 가져오지 못한다.

WORKDIR

컨테이너 내부에서 작업할 경로를 명시한다. 이전 챕터의 run, create 명령어에서 살펴보았던 -w, --workdir 과 동일한 역할을 한다고 보면 된다.

이후 COMMAND의 경우 WORKDIR에서 실행되며, 해당 부분을 명시하지 않을경우 홈 디렉토리(~)로 설정된다.

RUN [COMMAND]

이전 챕터의 run 명령어와는 다른 녀석이다. 오히려 COMMAND부분에 속하며, 컨테이너 상에서의 다양한 명령어를 해당 키워드를 통해 입력할 수 있다.

해당 명령은 이미지의 기본 명령과는 독립적으로 (마치 exec에 입력한 명령어처럼) 동작한다.

RUN은 2가지의 입력 형식을 지원한다.

# Method 1
RUN <command>
# Method 2
RUN ["executable", "param1", "param2"]

Method 2 방식이 어려워 보이나, 단지 명령어를 띄워쓰기를 통해 구분해 놓은 것에 불구하다

예를 들어 아래 2가지의 커맨드는 완벽히 동일한 동작을 수행한다.

RUN ["/bin/bash", "-c", "echo hello"]
RUN /bin/bash -c "echo hello"

Caution

리스트 형식으로 명령을 실행하고자 할 때 백슬래쉬 문자(\)를 통해 경로를 입력하는 경우가 있다.

실제 명령어는 JSON방식으로 실행되는데 백슬레쉬 문자는 JSON에서 인정하는 문자가 아니기 떄문이며, 특히 Windows에서는 (\)가 구분 기호로 사용되는데, 그 경우 백슬래쉬(\)를 두 번 입력하여 이스케이프 시켜주어야 한다.

RUN ["c:\windows\system32\tasklist.exe"] # (X)
RUN ["c:\\windows\\system32\\tasklist.exe"] # (O)

ENTRYPOINT

컨테이너가 실행되었을 때 스크립트나 명령어를 실행한다.

해당 파일을 컨테이너로 만들었을 때, 컨테이너를 시작하거나(start), 생성과 실행을 동시에 할 때(run) 할 때 수행된다고 생각하면 된다.

실행 중인 컨테이너에 단지 명령어만 보낼 때(exec)에는 수행되지 않는다.

또 다른 특징 중 하나는 ENTRYPOINTDockerfile에서 단 한 번만 선언할 수 있다는 것이다.

CMD

RUN과 비슷한 역할을 수행하나, docker run 실행시 별도 COMMAND가 없을 때 기본적으로 수행되는 COMMAND라는 점에서 차이가 있다.

# Method 1
CMD ["executable","param1","param2"] # exec shape, recommanded
# Method 2
CMD ["param1","param2"]
# Method 3
CMD command param1 param2

Method 1에서 excutable을 생략할 수 있는데, 이 경우 ENTRYPOINT가 정의되어 있어야 한다.

ENV

컨테이너의 환경변수를 지정하는 역할을 수행하며, run, create 명령어에서 살펴보았던 -e 옵션과 역할이 동일하다.

# Usage
ENV [KEY]=[VALUE]

# Example
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

LABEL

컨테이너의 메타데이터를 넣고자 할 때 사용한다.

# Usage
LABEL <key>=<value> <key>=<value> <key>=<value> ...

key-value pair로 저장되며, 예를 들어 아래와 같이 작성할 수 있다.

LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \ # 멀티 라인은 백슬래쉬(\) 구분자를 통해 입력할 수 있다.
that label-values can span multiple lines."
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

만약 메타데이터를 보고 싶다면 아래와 같은 명령어를 통해 특정 이미지(myimage)의 메타데이터를 확인해볼 수 있다.

$ docker image inspect --format='' myimage
{
  "com.example.vendor": "ACME Incorporated",
  "com.example.label-with-value": "foo",
  "version": "1.0",
  "description": "This text illustrates that label-values can span multiple lines.",
  "multi.label1": "value1",
  "multi.label2": "value2",
  "other": "value3"
}

EXPOSE [PORT]/[PROTOCOL]

EXPOSE는 어떤 포트를 수신 대기할 것인지에 대한 명시를 하는 부분이다.

하지만 Dockerfile 이는 호스트의 포워딩 했다는 의미가 아니다.

그저 호스트에서 Container로 데이터를 보낼 때 어떤 PORT로 보낼 지 지정하는 것이다.

따라서 -p 옵션은 Dockerfile 하나만으로는 자동화 할 수 없다.

[PROTOCOL]은 데이터 전송을 어떤 프로토콜을 사용할 지에 대한 명세이다.

tcp, udp 중 하나를 고르면 된다. 디폴트 값은 tcp로 지정되어 있고, 특별한 경우가 아니라면 바꾸는 것을 권장하지 않는다.

TCP는 패킷 전송 간에 신뢰성을 보장하기 떄문이다.

COPY [OPTION] [HOST_PATH] [CONTAINER_PATH]

HOST_PATH에 있는 파일들을 CONTAINER_PATH로 복사하는 명령어이다. HOST_PATH는 경로 뿐 아니라 파일명도 포함할 수 있으며, ? (한 글자), * (여러 글자) 등의 와일드 카드 역시 사용할 수 있다.

상대경로

PATH에 상대경로를 입력할 경우 HOST_PATH는 파일의 위치가 기준이 되며, CONTAINERWORKDIR이 기준이된다.

와일드 카드 예

  • home*: home으로 시작하는 모든 파일 및 경로
  • *home*: home이 포함된 모든 파일 및 경로
  • ?home: home 앞에 한 글자가 있는 모든 파일 및 경로 (예: 1home, ahome)

--chown

--chown옵션을 통해 CONTAINER_PATH의 소유자를 결정할 수 있다.

내부적으로 실제 chown 명령어를 통해 조작하기 때문에 Linux 기반의 컨테이너에서만 실행이 가능하다.

ADD [HOST_PATH] [CONTAINER_PATH]

ADDCOPY와 거의 동일한 기능을 수행한다고 보면 된다.

COPY의 옵션과 내용을 소급 적용하며, ADD만의 추가적인 기능이 몇 있다.

  1. 복사하려는 대상이 압축 파일(.tar, .tar.gz)일 경우 해당 파일의 압축을 해제하여 복사한다.
  2. wget 등을 통하여 원격지의 파일을 복사대상으로 지정할 수 있다.

단 원격지의 파일은 600권한(사용자만 읽기 가능)을 갖습니다.

옵션을 자동화 한다?

이쯤되면 이 글을 보는 사람이 의심을 가져야 할 부분이 있다.

과연 DockerfileCONTAINER의 제작 과정을 전부 명세했다고 할 수 있는가?

정답은 물론 아니다.

Dockerfile이 컨테이너를 명세하기는 하지만, 컨테이너를 생성하는 옵션을 자동화하는 목적은 아니다.

create, run 명령어에서 사용하는 -p 옵션만 봐도 그렇다.

-p 옵션은 Host OS의 포트와 Container의 포트를 명세해야 하나, EXPOSE는 단순히 컨테이너의 개방 포트만을 명세할 뿐이다.

이외의 Host OS의 많은 옵션들을 Dockerfile이 처리하지 못한다.

이는 build가 컨테이너에 대한 명세이며, Host에 대한 명세가 아니기 때문이다.

당연히 이러한 불편함은, 이후 포스팅할 Docker Compose가 나오는 계기가 되었다.

Docker Ignore File

git을 사용해 본 경험이 있다면 .gitignore를 한 번 쯤은 써본 경험이 있을 것이다.

.dockerignore 파일도 당연히 비슷한 역할을 수행한다.

.gitignore 파일은 커밋이 돼지 않을 파일을 설정하는 것이 대부분인데, 여기서는, ADDCOPY를 통해 Host OS에서 복사되면 안되는 파일이나 폴더의 경로를 정의한다.

# 주석
*/temp*
*/*/temp*
temp?

Reference