본문 바로가기
IT & 데이터 사이언스/Data Engineering

[Docker] 도커파일의 개념과 기본

by 바른 호랑이 2024. 12. 11.
728x90
반응형

안녕하세요. 바른호랑이입니다.

이번 게시글에서는 Docker 파일에 대해서 알아볼 예정입니다.

게시글은 '시작하세요! 도커/쿠버네티스 친절한 설명으로 쉽게 이해하는 컨테이너 관리'를 기반으로 작성하였으니 참고 바랍니다.

Docker에서 개발한 애플리케이션을 컨테이너화할 경우 일반적으로 아래와 같은 순서로 진행됩니다.

 

컨테이너로 이미지를 생성하는 방법

1. 이미지(Ubuntu, CentOS 등)로 컨테이너를 생성

2. 애플리케이션을 위한 환경을 설치하고 소스코드 등을 복사해 잘 동작하는 것을 확인

3. 컨테이너를 이미지로 커밋

 

위의 방법을 사용하면 애플리케이션이 동작을 보장할 수는 있지만 환경을 구성하기 위해 일일이 수작업으로 패키지를 설치하고 소스코드를 Git에서 복제하거나 호스트에서 복사해야 하는 번거로움이 있습니다. Docker에서는 이를 보다 간단히 할 수 있게 하기 위해 일종의 파일 형식인 dockerfile을 생성할 수 있게 하고 이를 읽음으로써 이미지를 생성할 수 있게 하는 방법을 지원하며, 이를 그림으로 표현해 보면 아래와 같습니다.

Dockerfile로 이미지를 생성하는 방법

 

 완성된 이미지를 생성하기 위해 컨테이너에 설치해야 하는 패키지, 추가해야 하는 소스코드, 실행해야 하는 명령어와 셸 스크립트 등을 하나의 파일에 기록한 후 이를 읽어 작업을 수행한 뒤 이미지로 만들어낸다고 이해하면 되며 이와 같은 작업을 기록한 파일을 Dockerfile이라고 합니다. Dockerfile을 사용하면 직접 컨테이너를 생성하고 이미지로 커밋해야 하는 번거로움을 덜 수 있을뿐더러 같은 개발 도구를 통해 애플리케이션의 빌드 및 배포를 자동화할 수 있다는 작업을 가집니다.

생성한 이미지를 Docker Hub 등을 통해 배포할 경우 이미지 자체를 배포하는 대신 이미지를 생성하는 방법을 기록해 놓은 Dockerfile을 배포할 수도 있으며, 만약 배포되는 이미지를 신뢰할 수 없거나 직접 이미지를 생성하여 사용하고 싶은 경우 Dockerfile을 활용하여 빌드하는 것도 하나의 방법입니다. 장기적인 관점에서 봤을 때 Dockerfile을 작성하여 이미지를 생성하는 것이 이미지의 빌드, 배포 측면에서도 유리하며 이는 이미지를 활용하는 방법 보다 Dockerfile을 활용한 방법이 애플리케이션에 필요한 패키지 설치를 명확히 할 수 있고, 이미지 생성의 자동화 및 배포에 있어서 강점을 가지기 때문입니다.

 

컨테이너에서 수행해야 할 작업을 명시하는 Dockerfile에 작업들을 정의하기 위해서는 우선 Dockerfile에서 쓰이는 명령어를 알아야 합니다. 이를 보다 실제적으로 알아보기 위해 Dockerfile로 웹 서버 이미지를 생성하는 실습을 진행해 보았습니다.

# create html file
sudo echo test >> test.html

# check file
ls

# create Dockerfile
sudo nano Dockerfile

# contents
FROM ubuntu:24.04
LABEL maintainer "righttiger <righttiger@email.com>"
LABEL "purpose"="practice"
RUN apt update
RUN apt 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

일반적으로 Dockerfile를 읽어 들일 때에는 현재 위치에 있는 Dockerfile을 사용하기에 테스트로 사용할 html 파일과 Dockerfile을 위의 내용으로 작성해 주었습니다. Dockerfile에서는 기본적으로 FROM, RUN, ADD를 사용하며, 위에서 아래로 한 줄씩 차례대로 실행됩니다. 사용되는 명령어들을 간단하게 설명하면 아래와 같습니다.

 

- FROM

생성할 이미지의 베이스가 될 이미지를 뜻합니다. FROM 명령어는 Dockerfile을 작성할 때 반드시 입력해야 하며 이미지 이름의 포맷은 docker run 명령어에서 이미지 이름을 사용했을 때와 같습니다. 만약 사용하려는 이미지가 없다면 자동으로 pull 합니다.

- LABEL

이미지에 메타데이터를 추가합니다. 메타 데이터는 '키:값'의 형태로 저장되며, 여러 개의 메타데이터가 저장될 수 있습니다. 추가된 메타데이터는 docker inspect 명령어로 이미지의 정보를 구해서 확인할 수 있습니다.

- RUN

이미지를 만들기 위해 컨테이너 내부에서 명령어를 실행합니다. 실습에서는 아파치 웹서버를 설치하는 명령어를 입력해 주었습니다. 다만 Dockerfile을 이미지로 빌드하는 과정에서는 별도의 입력이 불가능하기 때문에 설치여부를 지정하는 옵션을 yes로 설정해주어야 합니다. 만약 이미지를 빌드할 때 별도의 입력을 받아야 하는 RUN이 있으면 build 명령어는 이를 오류로 간주하고 build를 종료합니다. 일부 명령어는 RUN ["실행 가능한 파일", "명령줄 인자 1", "명령줄 인자 2", ~]과 같이 배열의 형태로도 입력이 가능한데 위의 실습에서는 이를 활용하여 RUN ["/bin/bash", "-c", "echo hello >> test2.html"]을 입력하여 /bin/bash 셸을 이용하여 echo hello >> test2.html 명령어를 실행해 주었습니다.

- ADD

파일에 이미지를 추가합니다. 추가하는 파일은 Dockerfile이 위치한 디렉터리에서 가져오며, JSON 배열의 형태로 ["추가할 파일 이름",..... "컨테이너에 추가될 위치"]와 같이 사용할 수 있습니다. 추가할 파일명은 여러 개를 지정할 수 있고, 실습에서는 test.html파일을 /var/www/html 디렉터리에 추가해 주었습니다.

-  WORKDIR

명령어를 실행할 디렉터리를 나타냅니다. bash 셸에서 cd 명령어를 입력하는 것과 같은 기능을 합니다. 만약 WORKDIR 명령어를 여러 번 사용하면 cd 명령어를 여러번 사용한 것과 같으며, WORKDIR /var/www/html 명령어는 WORKDIR /var 과 WORKDIR www/html 명령어를 실행한 것과 같습니다.

- EXPOSE

Dockerfile의 빌드로 생성된 이미지에서 노출할 port를 설정합니다. 다만 EXPOSE로 설정한 이미지로 컨테이너를 생성하였다고 해도 반드시 해당 port가 호스트의 port와 바인딩되는 것은 아니며, 단지 컨테이너의 80번 port를 사용할 것임을 나타냅니다. 일반적으로 EXPOSE는 컨테이너를 생성하는 run 명령어에서 모든 노출된 컨테이너의 포트를 호스트에 Publish 하는 -P 플래그와 함께 사용됩니다.

- CMD

컨테이너가 시작될 때마다 실행할 명령어를 설정하며 Dockerfile에서 한 번만 사용할 수 있습니다. apachectl -DFOREGROUND 커맨드를 내장하면 컨테이너를 생성할 때 별도의 커맨드를 입력하지 않아도 컨테이너가 시작될 때 자동으로 아파치 웹 서버가 실행됩니다. apache 웹 서버는 하나의 터미널을 차지하는 포그라운드 모드로 실행되기 때문에 -d 옵션을 사용해 detached 모드로 컨테이너를 생성해야 합니다.

run 명령어의 이미지 이름 뒤에 입력하는 커맨드와 같은 역할을 하지만 docker run 명령어에서 커맨드 명령줄인자를 입력하면 Dockerfile에서 사용한 CMD 명령어는 run 커맨드로 덮어 씝니다. CMD의 입력은 JSON 배열 형태인 ["실행 가능한 파일", "명령줄 인자 1", "명령줄 인자 2"......] 형태로 사용됩니다.

 

# create image using Dockerfile
# -t: 생성될 이미지의 이름을 설정하며 해당 옵션을 사용하지 않으면 16진수 형태의 이름으로 이미지가 저장됨.
sudo docker build -t mybuild:0.0 ./

생성한 Dockerfile과 html 파일을 활용하여 새로운 image를 build 해보았습니다. -t 태그를 사용하여 이미지 이름을 설정해 주었고, build 명령어 끝에 Dockerfile이 저장된 경로를 입력해 준 후 실행해 주었습니다. 이미지를 build 해준 후에는 정상적으로 생성되었는지 확인해 보기 위해 images명령어를 통해 확인해 주었습니다. 

# create container 
sudo docker run -d -P --name myserver mybuild:0.0

-P 옵션은 이미지에 설정된 EXPOSE의 모든 포트를 호스트에 연결하도록 설정합니다. 실습에서는 EXPOSE를 80번으로 설정했기에 이는 이미지에 '컨테이너의 80번 포트를 사용한다'라는 것을 의미합니다. -P 옵션의 경우 EXPOSE로 노출된 포트를 호스트에서 사용 가능한 포트에 차례로 연결하므로 해당 컨테이너가 호스트의 어떤 포트와 연결됐는지 확인하려면 docker ps 명령어를 사용 하거나 docker port 명령어를 사용하면 됩니다.

# check image using lable value
sudo docker images --filter "label=purpose=practice"
sudo docker images --filter "label=maintainer=righttiger <righttiger@email.com>"

Dockerfile에 이미지의 label을 설정해 주면 위와 같이 --filter 옵션을 사용해 해당 label을 가지는 이미지만을 출력하는 것 또한 가능합니다. label의 경우 컨테이너, 도커 엔진 등에 메타데이터를 추가하는 것을 지원해 주며 docker run 명령어에서는 --label 옵션으로도 사용이 가능합니다. --filter 옵션과 연계하여 원하는 조건의 컨테이너, 이미지 등을 쉽게 찾을 수 있게 도와주기에 적절하게 사용해 주는 것이 필요합니다.

 

일반적으로 이미지 빌드를 시작하면 Docker는 가장 먼저 build 컨텍스트를 읽어 들입니다. 빌드 컨텍스트란 이미지를 생성하는 데 필요한 각종 파일, 소스코드, 메타데이터 등을 가지고 있는 디렉터리를 의미하며, Dockerfile이 위치한 디렉터리가 빌드 컨텍스트가 됩니다. 이와 같은 빌드 컨텍스트는 Dockerfile에서 빌드될 이미지에 파일을 추가할 때 사용되며, 파일 추가는 ADD와 COPY 명령어등을 활용해 이루어집니다. 실습에서는 빌드 경로를 ./로 지정함으로써 현재 위치한 디렉토리를 빌드 컨텍스트로 지정해주었으며, 빌드 컨텍스트를 지정한 후에는 미리 생성해 두었던 test.html 파일을 ADD 명령어를 통해 추가해 주었습니다.

컨텍스트는 build 명령어의 맨 마지막에 지정된 위치에 있는 파일을 전부 포함합니다. git과 같은 외부 URL에서 Dockerfile을 읽어 들인다면 해당 Repository에 있는 파일과 서브 모듈을 포함 합니다. 그러므로 Dockerfile이 위치한 곳에는 이미지 빌드에 필요한 파일만 있는 것이 바람직한데, 이는 컨텍스트에는 단순 파일뿐만 아니라 하위 디렉터리도 전부 포함하게 되므로 빌드에 불필요한 파일이 포함된다면 빌드 속도가 느려질뿐더러 호스트의 메모리를 지나치게 점유할 수도 있기 때문입니다. 이를 방지하기 위해 git에서 사용하는 .gitignore와 유사한 기능을 하는 .dockerignore라는 파일을 작성하여 사용해야 하며, 해당 파일을 작성하면 빌드 시 해당 파일에 명시된 파일을 컨텍스트에서 제외합니다. 

# create .dockerignore file
sudo nano .dockerignore

# contents
test2.html
*.html
*/*.html
test.htm?

컨텍스트에서 제외할 파일의 경로는 Dockerfile이 존재하는 경로를 기준으로 합니다. 위의 작성한 .dockerignore 파일을 해석해 보면 Dockerfile이 위치한 디렉터리를 기준으로 해당 위치에 존재하는 test2.html 파일, .html로 끝나는 모든 파일,  한 단계 하위 디렉터리에 위치하면서 .html로 끝나는 모든 파일 그리고 test.htm으로 시작하면서 맨 뒷자리 값을 가지는 모든 파일(test.htma, test.htmb 등)을 제외한다는 것을 알 수 있습니다. 만약 *(와일드카드)를 사용하여 특정 포맷으로 해당하는 파일들을 제외(*.html)하면서 예외적으로 특정 파일만 남겨두고 싶다면 !test.html과 같이 화이트리스트 형태로 사용해야 합니다.

 

build 명령어는 Dockerfile에 기록된 대로 컨테이너를 실행한 뒤 완성된 이미지를 만들어 냅니다. 이미지를 만드는 과정은 하나의 컨테이너에서 일어나는 것은 아닌데 이는 ADD, RUN과 같은 명령어들이 실행될 때를 살펴보면 알 수 있습니다. 각 Step은 Dockerfile에 기록된 명령어에 해당하며, Dockerfile에서 명령어 한 줄이 실행될 때마다 이전 Step에서 생성된 이미지에 의해 새로운 컨테이너가 생성됩니다. 즉, Dockerfile에 적힌 명령어를 수행하고 다시 새로운 이미지 레이어로 저장된다고 할 수 있으며, 이를 그림으로 표현해 보면 아래와 같습니다.

따라서 이미지의 빌드가 완료되면 Dockerfile의 명령어 줄 수만큼의 레이어가 존재하게 되며, 중간에 컨테이너도 같은 수만큼 생성되고 삭제됩니다. 중간에 출력되는 Removing intermediate container .... 는 중간에 이미지 레이어를 생성하기 위해 임시로 생성된 컨테이너를 삭제하는 것이고, 삭제되기 전 출력되는 ID는 커밋된 이미지 레이어를 의미합니다.

 

# create Dockerfile2 
sudo nano Dockerfile2

# contents
FROM ubuntu:24.04
LABEL maintainer "righttiger <righttiger@email.com>"
LABEL "purpose"="practice"
RUN apt update

# build image using Dockerfile2
sudo docker build -f Dockerfile2 -t mycache:0.0 ./

한 번 이미지 빌드를 마치고 난 후 같은 빌드를 진행하면 이전 이미지 빌드에 사용했던 캐시를 사용합니다. 위와 같이 Dockerfile로 작성한 내용을 일부 지운 Dockerfile2를 생성한 후 -f(--file) 옵션을 사용하여 이미지 빌드시 사용할 Dockerfile을 Dockerfile2로 지정한 후 이미지를 빌드해 보면 cache를 사용하는 것을 확인할 수 있습니다. 이는 같은 명령어를 여러 번 실행해야 하는 여러 개의 이미지를 빌드하거나 빌드 도중 Dockerfile의 문법과 기타 오류가 발생했을 경우 불필요하게 명령어를 실행하는 것을 방지해 줍니다. 만약 이미지 빌드 도중에 오류가 발생하면 이미지 레이어 생성을 위해 생성된 임시 컨테이너가 삭제되지 않은 채로 남게 되며 <none>:<none>으로 이미지가 생성되는데 이 경우 docker rmi 명령어를 통해 해당 이미지를 삭제해주어야 합니다.

예외적으로 캐시 기능이 필요하지 않은 경우도 있는데 github와 같은 소스코드 저장소에서 git clone 명령어를 사용해 빌드를 할 때가 이에 해당합니다. Dockerfile에 RUN git clone ....을 사용해 이미지를 빌드했다면 RUN에 대한 이미지 레이어를 계속 캐시로 사용하게 되는데 이 경우 실제 git 저장소에서 리비전 관리가 일어나도 매번 빌드를 할 때마다 고정된 소스코드를 사용하게 되는 문제가 발생합니다. 이를 방지하기 위해서는 --no-cache 옵션을 추가하여 build를 진행해 주면 되며, 해당 옵션을 사용하면 기존 빌드에 사용된 캐시를 사용하지 않고 Dockerfile을 처음부터 다시 이미지 레이어로서 빌드합니다.

sudo docker build --no-cache -t mybuild:0.0 .

만약 Dockerfile을 확장해서 사용한다면 기존의 Dockerfile로 빌드한 이미지를 빌드 캐시로 사용하는 등 캐시로 사용할 이미지를 직접 지정할 수도 있습니다. 예를 들어 도커 허브의 nginx 공식 저장소에서 nginx:latest 이미지를 빌드하는 Dockerfile에 일부 내용을 추가해 사용하면 로컬의 nginx:latest 이미지를 캐시로 사용할 수도 있습니다.

sudo docker build --cache-from nginx -t my_extend_nginx:0.0 .

 

일반적으로 애플리케이션을 빌드할 때는 많은 의존성 패키지와 라이브러리를 필요로 하는데, 예를 들어 Go로 작성된 소스코드를 빌드하기 위해서는 Go와 관련된 빌드 툴과 라이브러리가 미리 설치되어 있어야 합니다. Dockerfile로 Go 소스코드를 빌드하기 위해서는 Go와 관련된 도구들이 설치된 이미지를 FROM에 명시한 뒤 RUN 명령어로 소스코드를 컴파일하는 방법을 사용할 수 있습니다.

# create main.go file
sudo nano main.go

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

# create Dockerfile
sudo nano Dockerfile

# contents
FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
CMD {".mainApp"}

# build image using Dockerfile
sudo docker build . -t go_helloworld

위의 파일들로 이미지를 생성해 보면, 단순히 Hello World를 출력하는 프로그램을 실행하는 이미지임에도 이미지의 크기가 800MB에 달하는 것을 확인할 수 있는데 이는 실제 실행 파일의 크기는 매우 작지만 소스코드 빌드에 사용된 각종 패키지 및 라이브러리가 불필요하게 이미지의 크기를 차지하기 때문입니다. 이와 같은 문제를 해결하는 방법으로는 Multi-stage 빌드 방법을 사용할 수 있는데 해당 방법은 하나의 Dockerfile 안에 여러 개의 FROM 이미지를 정의함으로써 빌드 완료 시 최종적으로 생성될 이미지의 크기를 줄이는 역할을 해줍니다. 이를 실제로 적용해 보기 위해 Dockerfile을 수정한 후 이미지를 build 해보았습니다.

# change Dockerfile contents
sudo nano Dockerfile

# contents
FROM 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"]

# build image
sudo docker build . -t go_helloworld:multi-stage

변경한 Dockerfile을 살펴보면 2개의 FROM을 통해 2개의 이미지가 명시되었고, 첫 번째 FROM에 명시된 golang 이미지는 이전과 동일하게 main.go 파일을 /root/mainApp으로 빌드하였습니다. 두 번째 FROM 아래에서 사용된 COPY 명령어는 첫 번째 FROM에서 사용된 이미지의 최종 상태에 존재하는 /root/mainApp 파일을 두 번째 이미지인 alpine:latest에 복사하게 작성하였는데 이때 쓰인 --from=0은 첫 번째 FROM에서 빌드된 이미지의 최종 상태를 의미합니다. 즉, 첫 번째 FROM 이미지에서 빌드한 /root/mainApp파일을 두 번째 FROM에 명시된 이미지인 alpine:latest 이미지에 복사하는 형태로 변경하였다는 것을 알 수 있습니다. alpine이나 busybox와 같은 이미지는 ubuntu나 CentOS에 비해 이미지 크기가 매우 작지만 기본적인 프로그램 실행에 필요한 필수적인 런타임 요소가 포함되어 있는 리눅스 배포판 이미지로 이를 활용하면 경량화된 애플리케이션 이미지를 생성할 수 있다는 장점이 있습니다. 실제로 빌드한 후 이미지의 크기를 확인해 보면 이전 이미지와 동일한 역할을 하는 이미지임에도 이미지의 최종 크기가 매우 줄은 것을 확인할 수 있습니다. 이와 같이 멀티 스테이지 빌드는 반드시 필요한 실행 파일만 최종 이미지 결과물에 포함시킴으로써 이미지 크기를 줄일 때 유용하게 사용할 수 있습니다.

멀티 스테이지 빌드를 사용하는 Dockerfile은 2개 이상의 이미지를 사용할 수 있으며, 각 이미지는 명시된 순서대로 0, 1, ...... 순으로 구분되어 사용됩니다. 이를 활용하면 여러 개의 이미지를 사용해 멀티 스테이지 빌드를 활용할 수 있습니다. 만약 특정 단계의 이미지에 별도의 이름을 부여하고 싶다면 as를 사용하여 명칭을 붙여줄 수도 있습니다.

# create Dockerfile2
sudo nano Dockerfile2

# contents using as option
FROM golang as builder
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
FROM alpine:latest
WORKDIR /root
COPY --from=builder /root/mainApp .
CMD ["./mainApp"]

# build image
sudo docker build . -f Dockerfile2 -t go_helloworld:multi-stage_2

728x90
반응형

'IT & 데이터 사이언스 > Data Engineering' 카테고리의 다른 글

[Docker] 도커 이미지  (4) 2024.10.08
[Docker] 컨테이너 자원 할당  (1) 2024.08.29
[Docker] 컨테이너 로깅  (0) 2024.08.26
[Docker] Docker 네트워크  (1) 2024.08.08
[Docker] Docker 볼륨  (5) 2024.08.05

댓글