본문 바로가기
DevOps/Docker

6. Dockerfile

by 비어원 2022. 5. 14.
728x90

컨테이너 이미지를 만들기 위해서는 (1). Ubuntu 등 아무것도 설치되지 않은 Base Image로 컨테이너를 생성하고 (2). 해당 컨테이너에 애플리케이션을 설치한 후 (3). 애플리케이션을 설치한 컨테이너를 커밋하여 새로운 이미지를 생성하는 작업을 거친다. 이렇게 이미지를 만드는 것은 과정이 복잡하고 수동적이다.

 

다행히도 도커에서는 위와 같은 일련의 과정들을 쉽게 정의하고 수행할 수 있는 빌드 명령어를 제공한다. Dockerfile 이라는 파일에 이미지를 생성하기 위해 필요한 것들 (컨테이너에 설치해야 하는 패키지, 추가 소스코드, 명령어, 쉘 스크립트 등..) 을 기록하면 도커는 이 파일을 읽어 컨테이너에서 작업을 수행한 뒤 이미지를 생성한다.

 

Dockerfile을 사용하면 직접 컨테이너를 생성하고 컨테이너에 들어가서 추가 작업을 한 후 커밋해야 하는 번거로움을 덜 수 있으며 깃과 같은 개발도구를 통해 애플리케이션의 빌드와 배포를 자동화할 수 있다. 추가로 도커 허브에 Dockerfile을 배포할 수도 있다. 실제로 도커 허브에 올려져있는 대부분의 이미지는 Dockerfile을 같이 제공한다.

Dockerfile 작성

Dockerfile에는 컨테이너에서 수행해야 하는 작업을 명시한다. 일단 간단한 예시를 보면서 Dockerfile을 작성하는 방법에 대해 알아보자.

일단 간단히 Dockerfile을 통해 웹 서버 이미지를 생성해보자. 수행에 앞서 별도의 디렉터리를 생성하고 디렉터리 내에 HTML 파일을 미리 만들어 두자.

$ mkdir dockerfile-study && cd dockerfile-study
$ echo test >> test.html

 

그리고 Dockerfile을 하나 생성하자.

$ vi Dockerfile
FROM ubuntu:14.04
MAINTAINER beer1
LABEL "purpose"="practice"
RUN apt-get update
RUN apt-get install apache2 -y
ADD test.html /var/www/html
WORKDIR /var/www/html
RUN ["/bin/bash", "-c", "echo hello >> test2.html"]
EXPOSE 80
CMD apachectl -DFOREGROUND
  • FROM: 베이스 이미지이다. 필수로 작성해야 한다. 이미지가 로컬에 없다면 자동으로 pull한다.
  • MAINTAINER: 이미지를 생성한 개발자의 정보를 나타낸다. 1.13.0 버전 이후로는 사용하지 않는다. 대신 LABEL 을 통해 표현할 수 있다. LABEL maintainer "beer1" <email>
  • LABEL: 이미지에 메타데이터를 추가한다. Key-value 형식이며 여러 개 등록 가능하다. docker inspect 명령어로 메타데이터를 볼 수 있다.
  • RUN: 이미지를 만들기 위해 컨테이너 내부에서 실행하는 명령어를 등록한다. 이 때 입력을 받아야 하는 명령어를 등록할 경우 오류로 간주하고 빌드를 종료한다. 그래서 입력을 받는 명령어를 넣지 않도록 주의해야 한다.
  • ADD: 파일을 이미지에 추가한다. ADD {호스트 현재 상대경로 or 절대경로} {도커 절대경로}
  • WORKDIR: 명령어를 실행할 디렉터리를 나타낸다. bash에서 cd 명령어와 같다.
  • EXPOSE: Dockerfile의 빌드로 생성된 이미지에서 노출할 포트를 설정한다. 이 명령어를 통해 해당 포트가 호스트의 포트와 바인딩되는 것은 아니며 단지 컨테이너에서 포트를 사용함을 나타낼 뿐이다.
  • CMD: 컨테이너가 시작될 때 마다 실행할 명령어를 설정하며 Dockerfile에서 한 번만 등록할 수 있다. 이는 컨테이너가 시작될 때 실행할 기본 명령어이며, docker run 에서 커맨드 명령 인자를 등록하면 CMD에서 등록된 명령어가 아닌 커맨드 명령 인자로 등록된 명령어로 덮어쓰인다.

 

Dockerfile 빌드

Dockerfile을 생성한 후 다음 명령어를 사용하여 Dockerfile으로 빌드를 해보자.

$ docker build -t dockerfile-test:0.0 .
  • -t: 생성될 이미지의 이름을 설정한다.

 

빌드를 하면 다음과 같은 내용이 출력된다.

Sending build context to Docker daemon  3.072kB
Step 1/10 : FROM ubuntu:14.04
 ---> 13b66b487594
Step 2/10 : MAINTAINER beer1
 ---> Running in ffe16cacf144
Removing intermediate container ffe16cacf144
 ---> 586645ed9579
Step 3/10 : LABEL "purpose"="practice"
 ---> Running in 8cb27d05ffbc
Removing intermediate container 8cb27d05ffbc
 ---> 5eb414ba4857
Step 4/10 : RUN apt-get update
 ---> Running in c79546d4df67
Get:1 http://security.ubuntu.com trusty-security InRelease [65.9 kB]
...
Removing intermediate container c79546d4df67
 ---> e941fb9bdb77
Step 5/10 : RUN apt-get install apache2 -y
 ---> Running in dd658c0b7fb7
...
Removing intermediate container dd658c0b7fb7
 ---> 04bbfc237b00
Step 6/10 : ADD test.html /var/www/html
 ---> bfc5986a78a3
Step 7/10 : WORKDIR /var/www/html
 ---> Running in ed182d362d1a
Removing intermediate container ed182d362d1a
 ---> 8468519816a7
Step 8/10 : RUN ["/bin/bash", "-c", "echo hello >> test2.html"]
 ---> Running in a01082d83ab5
Removing intermediate container a01082d83ab5
 ---> edf56e69b8ff
Step 9/10 : EXPOSE 80
 ---> Running in c1590aa15e4a
Removing intermediate container c1590aa15e4a
 ---> 3aef271ca375
Step 10/10 : CMD apachectl -DFOREGROUND
 ---> Running in 3359c9871b2e
Removing intermediate container 3359c9871b2e
 ---> 9bb16641162b
Successfully built 9bb16641162b
Successfully tagged dockerfile-test:0.0 

 

이미지 생성이 완료되었으면 컨테이너를 실행해보자.

$ docker run -d -P --name test-server dockerfile-test:0.0
  • -P EXPOSE에서 설정된 모든 포트를 호스트에 연결하도록 설정한다.

 

실제로 컨테이너의 포트가 호스트의 어떤 포트와 연결되어있는지 확인하려면 다음 명령어를 사용하면 된다.

$ docker port test-server
80/tcp -> 0.0.0.0:49153
80/tcp -> :::49153

 

레이블을 가지고 도커 이미지를 필터링할 수도 있다.

$ docker images --filter "label=purpose=practice"
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
dockerfile-test   0.0       9bb16641162b   3 minutes ago   221MB

 

빌드 과정

Dockerfile을 통해 이미지 빌드를 시작하면 도커는 먼저 빌드 컨텍스트를 읽어들인다. 빌드 컨텍스트는 이미지를 생성하는데 필요한 각종 파일, 소스코드, 메타데이터 등을 담고 있는 디렉터리를 의미하며 Dockerfile이 위치한 디렉터리가 빌드 컨텍스트가 된다.

 

빌드 컨텍스트는 Dockerfile에서 빌드될 이미지에 파일을 추가할 때 사용된다. 이미지에 파일을 추가하는 방법은 ADDCOPY가 있는데 이 명령어들은 빌드 컨텍스트의 파일을 이미지에 추가한다.

 

빌드 컨텍스트에 대한 정보는 이미지를 빌드할 때 출력되는 내용 중 맨 처음에 나온다.

Sending build context to Docker daemon  3.072kB
...

 

컨텍스트는 build 명령어의 맨 마지막에 지정된 위치에 있는 파일을 모두 포함한다. 이전에 사용했던 명령어를 보면 다음과 같다.

$ docker build -t dockerfile-test:0.0 .

 

. 은 현재 디렉터리를 의미하며 현재 디렉터리 구조는 이렇게 되어있다. 즉, 빌드 컨텍스트에는 다음의 파일들이 모두 포함된다.

$ ls
Dockerfile  test.html

따라서 Dockerfile이 위치한 곳에서는 이미지 빌드에 필요한 파일만 있는 것이 좋다. 컨텍스트는 지정한 디렉터리 뿐 아니라 그 디렉터리의 하위 디렉터리도 전부 포함한다. 그래서 빌드에 필요하지 않은 파일이 해당 디렉터리에 포함된다면 빌드 속도에 영향을 줄 수 있으며 호스트의 메모리를 많이 점유하게 될 수 있다.

 

.dockerignore

어쩔 수 없이 Dockerfile이 위치한 곳에 빌드에 불필요한 파일을 둬야 한다면 .gitignore와 비슷한 기능을 사용할 수 있다. 도커에서는 .dockerignore 라는 파일을 사용할 수 있으며 이 파일은 컨텍스트의 최상위 경로 (즉, Dockerfile이 위치한 경로)에 위치해야 한다.

 

일단 Dockerfile이 위치한 곳에 .dockerignore 파일을 만들어보자.

$ vi .dockerignore
test2.html
*.html
*/*.html
test.htm?

 

컨텍스트에서 제외할 파일의 경로는 Dockerfile이 존재하는 경로를 기준으로 하면 된다. 위의 내용대로라면 test2.html (test2.html), 해당 경로에 있는 모든 html 확장자 파일(*.html), 한 단계 상위 디렉터리에 있는 모든 html 파일 (*/*.html), test.htm 을 접두어로 두고 임의로 1자리 문자가 들어간 파일 (test.htm?) 을 제외한다.

 

.dockerignore의 제외 목록에 해당되지만 특수한 파일만 포함하도록 하려면 !을 사용하면 된다.

*.html
!test*.html

위의 예시는 해당 경로의 모든 html 파일을 제외 (*.html) 시키지만 test로 시작하는 이름의 html파일은 제외시키지 않는다.(!test*.html)

 

Dockerfile을 이용한 컨테이너 생성과 커밋

Build 명령어로 Dockerfile에 기록된 대로 컨테이너에 명령어를 실행한 뒤 이미지를 완성시킨다. 하지만 이미지로 만드는 과정이 하나의 컨테이너에서 일어나는 것은 아니다. 출력 결과를 보면 이렇게 나오는 것을 알 수 있다.

Sending build context to Docker daemon  3.072kB
Step 1/10 : FROM ubuntu:14.04
 ---> 13b66b487594
Step 2/10 : MAINTAINER beer1
 ---> Running in ffe16cacf144
Removing intermediate container ffe16cacf144
 ---> 586645ed9579
Step 3/10 : LABEL "purpose"="practice"
 ---> Running in 8cb27d05ffbc
Removing intermediate container 8cb27d05ffbc
 ---> 5eb414ba4857
...

 

출력 결과에는 여러가지 Step으로 나뉘어지고, 각 Step은 Dockerfile에 기록된 명령어 하나하나에 해당한다. 각 명령어마다 새로운 컨테이너가 하나씩 생성되며 이를 이미지로 커밋한다. 즉, Dockerfile에서 명령어 한 줄이 실행될 때 마다 이전 Step에서 생성된 이미지에 의해 새로운 컨테이너가 생성되며 Dockerfile에 적힌 명령어를 수행하고 다시 새로운 이미지 레이어로 저장된다.

이미지 빌드가 완료되면 Dockerfile의 명령어 개수만큼 레이어가 존재하게 되며 컨테이너도 같은 수만큼 생성되고 삭제된다. 출력 결과에서 Removing intermediate container ... 는 중간에 이미지 레이어를 생성하기 위해 임시로 생성한 컨테이너를 삭제하는 것이며 삭제 후 출력되는 id는 커밋된 이미지 레이어를 의미한다.

 

캐시를 이용한 이미지 빌드

한 번 이미지 빌드를 하고, 다시 같은 빌드를 진행하면 이전의 이미지 빌드에서 사용했던 캐시를 사용한다. Dockerfile을 하나 복사하여 Dockerfile2로 만들고 내용을 일부 지워보자.

$ cp Dockerfile Dockefile2
$ vi Dockerfile2
FROM ubuntu:14.04
MAINTAINER beer1
LABEL "purpose"="practice"
RUN apt-get update

 

그리고 Dockerfile2의 내용으로 빌드를 해보자.

$ docker build -f Dockerfile2 -t dockerfile-cache:0.0 .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM ubuntu:14.04
 ---> 13b66b487594
Step 2/4 : MAINTAINER beer1
 ---> Using cache
 ---> 586645ed9579
Step 3/4 : LABEL "purpose"="practice"
 ---> Using cache
 ---> 5eb414ba4857
Step 4/4 : RUN apt-get update
 ---> Using cache
 ---> e941fb9bdb77
Successfully built e941fb9bdb77
Successfully tagged dockerfile-cache:0.0
  • -f 명령어로 파일을 지정할 수 있다. -f 플래그가 없다면 Dockerfile 이름의 파일을 사용한다.
  • 출력 결과를 보면 Using cache 라는 내용이 나오며 캐시를 사용하여 별도의 빌드 과정을 생략하는 것을 알 수 있다.

 

하지만 캐시 기능을 사용하지 않아야 하는데 캐시를 사용할 경우에는 원하는 결과로 빌드가 되지 않을 수도 있다. 예를 들어 다음과 같은 명령어이다.

RUN git clone ...

 

git 저장소 내용은 변경되었지만 도커파일 명령어가 변경되지 않아 이 명령어는 캐시를 사용하여 명령어를 사용하지 않을 것이고 변경된 git 저장소 내용이 반영이 되지 않을 것이다. 이러한 경우 --no-cache 플래그를 사용하여 빌드 시 캐시를 사용하지 않도록 설정할 수 있다.

$ docker build --no-cache -t dockerfile-cache:0.0 .

ADD, COPY 명령어는 캐시를 타지 않는다.

 

멀티 스테이지를 이용한 Dockerfile 빌드

Docker 17.05 버전 이상을 사용한다면 이미지의 크기를 줄이기 위해 멀티 스테이지 빌드 방법을 사용할 수 있다. 멀티 스테이지 빌드는 하나의 Dockerfile에 여러 개의 FROM 이미지를 정의함으로써 빌드 완료 시 최종적으로 생성될 이미지의 크기를 줄이는 역할을 한다.

예를 들어 go 소스코드를 작성한 뒤 빌드된 프로그램을 실행하는 도커 이미지를 만들어보자. 기존 방식대로라면 다음과 같이 만들 것이다.

 

main.go

package main
import "fmt"
func main() {
    fmt.Println("hello world")
}

Dockerfile

FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
CMD ["./mainApp"]
$ docker build -t go_helloworld .

 

위와 같은 방식으로 이미지를 만들었는데 단순 hello world만 출력하는 이미지 용량이 1G 근처까지 간다. 실제 실행 파일의 크기는 매우 작지만 소스코드 빌드에 사용된 각종 패키지 및 라이브러리가 불필요하게 이미지의 크기를 차지하고 있기 때문이다.

$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
go_helloworld                        latest    e8ef7620c31d   20 seconds ago   966MB

 

이번에는 멀티 스테이지 빌드를 사용해 이미지를 빌드해보자.

 

Dockerfile-multistage

iFROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go

FROM alpine:latest
WORKDIR /root
COPY --from=0 /root/mainApp .
CMD ["./mainApp"]

위의 Dockerfile 에서는 2개의 FROM을 통해 2개의 이미지가 명시되어있다. 첫 번째 FROM에 명시된 golang 이미지는 CMD를 제외하고는 맨 위의 이미지와 같은 내용이다.

두 번째 FROM에서 부터 보면 COPY 명령어를 사용하여 첫 번째 FROM에서 사용된 이미지의 최종 상태에 존재하는 /root/maiinApp 파일을 두 번째 이미지인 alpine:latest 에 복사한다. 이 때 --from=0 은 첫 번째 FROM 에서 빌드된 이미지의 최종 상태를 의미한다. 즉, 최종적으로 빌드한 결과물인 /root/mainApp 파일을 경량화된 alpine:latest 이미지에 복사하여 최종 이미지의 용량을 줄일 수 있다.

 

alpine이나 busybox와 같은 이미지는 우분투나 Centos에 비해 이미지 크기가 매우 작지만 기본적으로 프로그램 실행에 필요한 필수적인 런타임 요소가 포함된 리눅스 배포판 이미지이다. 이러한 이미지를 통해 이미지의 용량을 줄일 수 있다.

 

이제 이 파일을 통해 이미지를 만들어보자.

$ docker build -t go_helloworld_multi -f Dockerfile-multistage .
...

$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
go_helloworld_multi                  latest    5da0fb00e563   11 seconds ago   7.33MB
go_helloworld                        latest    e8ef7620c31d   15 minutes ago   966MB

멀티 스테이지로 이미지를 만드니까 용량이 대폭 감소되었다.

 

Dockerfile로 빌드할 때 주의점

  1. 하나의 명령어를 \ 로 나눠서 가독성을 높일 수 있도록 작성한다.
  2. .dockerignore 파일을 작성해 불필요한 파일을 빌드 컨텍스트에 포함시키지 않도록 한다.
  3. 빌드 캐시를 이용해 기존에 사용했던 이미지 레이어를 재사용할 수 있으면 한다.
  4. 불필요한 레이어가 늘어나는게 아닌지 고민하자.

 

불필요한 레이어가 늘어나는게 아닌지 고민하자.

Dockerfile을 아무렇게나 작성하면 저장공간을 불필요하게 차지하는 이미지나 레이어가 늘어나기 때문에 신경을 써야한다. 레이어는 앞서 말했듯이 Dockerfile에서의 명령어 단위가 레이어가 된다.

 

예를 들어, 잘못 작성된 Dockerfile을 소개하겠다.

FROM ubuntu:14.04
RUN mkdir /test
RUN fallocate -l 100m /test/dummy
RUN rm /test/dummy

 

Dockerfile 내용대로라면 최종 결과물은 ubuntu:14.04 이미지와 용량이 같아야 한다. 하지만 결과물은 용량이 훨씬 많아진다.

$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
layer-test                           latest    4ed6fecc595b   3 seconds ago    301MB
ubuntu                               14.04     13b66b487594   13 months ago    197MB

 

세 번째 명령어에서 100MB의 데이터를 생성하고 네 번째 명령어에서 그 데이터를 삭제했는데 용량은 100MB가 늘어난 채로 이미지가 생성되었다. 이유는 컨테이너를 이미지로 생성할 때 컨테이너에서 변경된 사항만 새로운 이미지 레이어로 생성하기 때문이다. rm 명령어로 데이터를 삭제한다고 하더라도 파일을 삭제했다 라는 변경사항으로 레이어가 새로 생성될 뿐 실제 100MB 크기의 파일은 이전 레이어에 남아있기 때문이다. (파일을 삭제했다는 변경사항을 저장한 레이어의 크기가 -100MB가 되진 않는다.)

 

이를 방지하기 위해서는 && 를 통해 RUN 명령어를 하나로 묶으면 된다.

FROM ubuntu:14.04
RUN mkdir /test && \
fallocate -l 100m /test/dummy && \
rm /test/dummy

 

위의 Dockerfile을 바탕으로 이미지를 만들면 ubuntu:14.04 이미지와 용량이 같게 생성된다.

$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
layer-test                           2         0abdfd529fc7   3 seconds ago    197MB
layer-test                           latest    4ed6fecc595b   9 minutes ago    301MB
ubuntu                               14.04     13b66b487594   13 months ago    197MB

 

728x90

'DevOps > Docker' 카테고리의 다른 글

Harbor Private Registry 소개 및 설치  (1) 2023.10.09
7. 도커 컨테이너 로그와 자원 할당 제한  (0) 2022.05.17
5. Docker Image  (0) 2022.05.14
4. Docker Network  (0) 2022.05.14
3. Docker Volume  (0) 2022.04.24

댓글