[LG CNS AM Inspire Camp 1기] Docker (3) - Dockerfile 유의사항

정성엽·2025년 2월 13일
0

LG CNS AM Inspire 1기

목록 보기
49/53
post-thumbnail

INTRO

어떻게 하면 Dockerfile을 효율적으로 사용할 수 있을까?

이번 포스팅에서는 효율적인 Docker 이미지 생성을 위한 Dockerfile 작성 방법에 대해 간단하게 정리해보자 👀


1. Docker 이미지 크기에 유의하자

도커를 이용한 배포는 네트워크를 통한 배포가 가능하다는 특징이 있다.

하지만, 이미지 파일이 너무 커진다면 네트워크를 통해 이미지를 다운로드하는 시간이 오래걸리게 된다.

이렇게되면 네트워크를 통한 빠른 배포라는 강점을 잃게 되는 것이다.

따라서 우리는 도커 이미지가 너무 커지는 것을 방지하면서 Dockerfile을 작성해야 한다.

💡 큰 도커 이미지

우선 일부러 크기가 큰 이미지를 생성해보자

Sample Code

# Dockerfile_100m
FROM ubuntu
RUN mkdir /test
RUN fallocate -l 100m /test/dummy
RUN rm /test/dummy

공간을 100MB 할당하여 /test/dummy 디렉터리를 생성한다.

이후, /test/dummy 파일을 삭제하는 명령어를 추가한다.

즉, 100MB짜리 디렉토리를 만들고 다시 지워버리기 때문에 결국 초기 상태와 다른게 없다.

그렇다면 이미지를 빌드해보고 사이즈를 확인해보자

◉ 커맨드 - 도커 이미지 빌드
docker image build -t falloc_100m -f Dockerfile_100m .

Dockerfile_100m이라는 도커파일을 이용하여 이미지를 빌드한다.

이후 image 크기를 확인해보면 다음과 같다.

Result View

보다시피 Base Layer인 ubuntu의 사이즈 (139MB)보다 falloc_100m 이미지의 사이즈(244MB)가 대략 100MB 정도 더 큰 모습을 볼 수 있다.

결과는 동일하지만 사이즈 차이가 나는 이유는 무엇때문일까?

바로 도커가 레이어드 아키텍처를 사용하기 때문이다.

즉, Dockerfile을 작성할 때 RUN이라는 명령어로 레이어를 하나씩 위로 쌓다보니 결과는 ubuntu와 같더라도 레이어가 추가되었기 때문에 사이즈 차이가 발생하는 것이다.

따라서, 우리는 Dockerfile을 작성할 때 RUN과 같은 지시어를 가급적 많이 사용하지 않도록 해야한다.

그럼 어떻게 리팩토링할 수 있을까?

💡 RUN 한번에 수행하기

&&\ 를 함께 사용하여 여러개의 RUN 명령어를 한번에 수행할 수 있다.

우선 도커파일 코드를 살펴보자

Sample Code

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

이처럼 && 는 AND연산자와 동일한 맥락으로 뒤의 명령어도 실행한다는 의미이고, \ 는 개행을 의미한다.

이런 방식으로 Dockerfile을 이해하기 쉽고 효율적으로 작성할 수 있다.

위 사진은 nginx의 github에서 가져왔는데, 실제로 무수한 개행을 사용하여 RUN 명령어 하나에 커맨드를 모두 실행하는 모습을 볼 수 있다!


2. 불필요한 도구를 설치하지 말자

실제 서비스를 배포하는 단계에 도달했다면 우리가 개발하면서 사용했던 도구들의 일부는 서비스 단계에서는 필요 없게된다.

예를들어 vi, gcc, javac, ifconfig 등의 개발 및 디버깅 용도로 사용하는 도구는 배포 단계에서는 필요가 없다.

따라서 만약 쉘을 사용하여 어떤 프로그램을 설치한다면 (ex. ubuntu에서 apache 설치) 설치 모듈에 따라 다르겠지만 불필요한 도구를 설치하지 않도록 설치하면 된다.

EX) RUN pip3 install --no-cache-dir -r requirements.txt

결국 티끌모아 태산이라는 이야기다!


3. 빌더 패턴 사용하기

예를들어, Java 기반의 어플리케이션을 컨테이너에 올리고 싶다면 Javac에 의해 컴파일된 결과물만 도커 파일에 추가하면 된다.

즉, 우리가 개발한 소스코드까지 모두 올리는 것이 아니라 컴파일러에 의해 빌드된 결과물만 올리는 방법을 의미한다.

우선, 간단한 go파일을 하나 생성하자

Sample Code

package main

import "fmt"

func main() {
	fmt.Println("Hello, Docker")
}

콘솔에 "Hello, Docker"를 출력하는 파일이다.

💡 Dockerfile_build

이제 hello.go 파일을 빌드하여 hello.exe를 생성하는 Dockerfile을 작성해보자

Sample Code

FROM golang
COPY hello.go .
RUN go env -w GO111MODULE=auto && \
    go build -o hello.exe .

◉ 커맨드 - 특정 도커 파일을 이용하여 이미지 빌드
docker image build -t hello_build -f Dockerfile_build .


-f : 기본적으로 Dockerfile이라는 이름의 파일을 이용하여 이미지를 빌드한다. 하지만, 여러개의 도커파일이 있을 경우 파일을 선택하기 위해 -f 옵션을 사용한다.
-f 옵션 뒤에 빌드하기위해 사용할 도커파일명을 지정해주면 된다.


Result View

VSCode로 작성한 hello.go 파일을 hello.exe로 빌드하는 Dockerfile을 작성했다.

사진과 같이 사이즈는 1.29GB에 해당하는 모습을 볼 수 있다.

컨테이너를 실행시켜서 빌드된 hello.exe를 직접 확인해보자

커맨드 - 컨테이너 실행 & hello.exe 확인
docker container run -it hello_build


Result View

가상터미널에서 확인해보면 hello.exe가 정상적으로 빌드되어 생성된 모습을 볼 수 있다!

💡 Dockerfile_run

이제 우리는 Dockerfile_build를 통해 생성한 hello.exe만 담은 컨테이너를 만들어야 한다.
(여기서 확장자가 .exe지만 실제로 실행파일은 아니다!)

즉, 빌드 과정에서 필요한 내용은 모두 제거하고 빌드된 결과물만 가지고 있는 컨테이너를 만들려고 한다.

어떻게 해야할까?

우선 Dockerfile_build를 기반으로 생성한 컨테이너에서 hello.exe를 가져와야 한다.

◉ 커맨드 - 컨테이너 내부에서 빌드된 hello.exe 가져오기
docker container cp 120a0b3d59d4:/go/hello.exe .


Result View

컨테이너식별자:대상파일 형태로 파일에 접근한다.
다음으로 . 을 사용하여 현재 디렉토리로 대상 파일을 Copy해오는 커맨드이다.

사진과 같이 정상적으로 hello.exe가 생겼다.

이렇게 가져온 hello.exe를 이용하여 이미지를 생성하는 Dockerfile_run을 작성해보자

Sample Code

FROM alpine
COPY hello.exe .
ENTRYPOINT [ "./hello.exe" ]

여기서 사용된 alpine은 실행에 필요한 부분만 모아둔 경량화된 리눅스이다.

hello.exe를 실행하기 위해 리눅스가 필요한데, 아주 작은 리눅스로 가져와서 이미지를 생성하도록 Dockerfile을 작성할 수 있다.

(ENTRYPOINT 는 나중에 정리할 건데 일단은 CMD 와 같이 실행할 명령어를 뒤에 적었다고 이해하면 된다.)

마찬가지로 이미지를 빌드하고 사이즈를 확인하면 다음과 같다.

Result View

빌드 파일과 경량화된 리눅스로 빌드된 이미지는 16.3MB의 사이즈를 갖는다.

반면, golang을 base layer로 hello.exe를 빌드하는 hello_build 이미지는 1.29GB의 용량을 차지한다.

따라서, 우리는 빌드된 결과물만 사용하도록 Dockerfile을 작성하면 이미지의 사이즈를 줄일 수 있다!

이처럼 buildrun 을 나누는 패턴을 바로 빌드 패턴이라고 한다.


4. 기존 빌드 패턴의 문제점

우리가 위에서 정리한 빌드 패턴에서는 이미지를 빌드하고 , 컨테이너를 실행하고 , 컨테이너 내부의 파일을 복사해서 가져오고 , 다시 이미지를 빌드하는 등... 의 작업을 하나씩 커맨드를 입력하여 처리했다.

이렇게 작업하면 너무 할일이 많기 때문에, 쉘 스크립트를 작성하여 한번에 해결해보자

💡 쉘 스크립트 작성

윈도우에서는 .cmd 확장자 파일을 생성하고, 맥에서는 .sh 확장자로 파일을 하나 생성하자

다음으로 우리가 이전에 수행했던 명령어들을 여기에 모두 적어보자

Sample Code

docker image build -t hello_build:v2 -f Dockerfile_build .
docker image build -t hello_run:v2 -f Dockerfile_run .
docker container create --name hello_build hello_build:v2
docker container cp hello_build:/go/hello.exe .
docker container create --name hello_run hello_run:v2
docker container rm hello_build
rm hello.exe

다음으로 우리가 작성한 쉘 스크립트가 존재하는 곳으로 디렉토리를 이동한 이후, 맥에서는 sh build_run.sh 명령어를 사용하여 쉘 스크립트를 실행시킬 수 있다.

Result View

보다시피 우리가 sh파일에 작성한 명령어를 하나씩 실행하여 최종적으로 hello_build를 출력하고 사라지는 모습을 볼 수 있다.

💡 다단계 빌드 Dockerfile

사실 커맨드를 한꺼번에 모아서 실행하는 것도 우리가 바라는 방법은 아닐 것이다.

왜냐하면, 결국 우리가 커맨드를 하나씩 모두 입력해야하며, .sh 파일을 별도로 생성하고 관리해야한다는 불편함이 존재하기 때문이다.

Docker에서는 이러한 문제점을 해결하기 위해 multi-state build Dockerfile 이라는 방법을 제공한다.

이는 우리가 이전에 작성한 2가지의 파일 (Dockerfile_build, Dockerfile_run)을 하나의 파일에 작성하여 관리하는 방법이다!

우선 이전에 작성했던 2개의 Dockerfile을 하나의 Dockerfile에 복붙해서 가져와보자

Sample Code

# Dockerfile
FROM golang
COPY hello.go .
RUN go env -w GO111MODULE=auto && \
    go build -o hello.exe .

FROM alpine
COPY hello.exe .
ENTRYPOINT [ "./hello.exe" ]

두번째 alpine에서는 golang을 기반으로 빌드한 결과물인 hello.exe 파일을 복사해서 가져와야 한다.

이 내용을 추가하여 코드를 수정해보면 다음과 같다.

Sample Code

FROM golang
COPY hello.go .
RUN go env -w GO111MODULE=auto && \
    go build -o hello.exe .

FROM alpine
COPY --from=0 /go/hello.exe .
ENTRYPOINT [ "./hello.exe" ]

두번째 COPY 커맨드쪽에서 --from=0 이라는 내용이 추가되었는데 이는 위에서 빌드된 이미지를 의미한다.

조금 더 쉽게 사용해보면 AS 라는 별칭을 추가하여 다음과 같이 코드를 수정할 수 있다.

Sample Code

FROM golang AS buildtime
COPY hello.go .
RUN go env -w GO111MODULE=auto && \
    go build -o hello.exe .

FROM alpine
COPY --from=buildtime /go/hello.exe .
ENTRYPOINT [ "./hello.exe" ]

이처럼 빌드된 각 결과물에 대하여 별칭을 추가하고 Dockerfile 내부에서 이 별칭을 이용하여 참조할 수 있는 것이다.

그러면 이 Dockerfile을 사용하여 이미지를 빌드하고 컨테이너를 실행시켜보자

Result View


Dockerfile은 이미지를 빌드하기 위해 사용하는 파일이므로, 이미지를 빌드하고 컨테이너를 실행하는 부분을 따로 수행했다.

마찬가지로 Hello, Docker가 정상적으로 출력되는 모습을 볼 수 있다.

여기서 주목해야할 부분은 멀티 스테이지 빌드 방식을 사용하게되면 맨 마지막에 작성된 이미지가 생성된다는 것이다.

즉, 결과적으로 생성되는 최종 이미지는 alpine 기반이며, 실행에 필요한 바이너리만 포함하므로 이미지 크기가 매우 작아진 모습을 볼 수 있다!


OUTRO

이번 포스팅에서는 효율적인 Dockerfile 작성 방법에 대해 알아보았다.

이미지 크기를 줄이기 위한 다양한 방법들을 실습을 통해 살펴보았는데, 특히 레이어를 최소화하고 불필요한 도구를 제거하며, 멀티 스테이지 빌드를 활용하는 것이 중요하다는 것을 알아두면 좋겠다.

처음에는 단순히 동작하는 Dockerfile을 작성하는 것에 집중했다면, 이제는 보다 효율적이고 최적화된 Docker 이미지를 만들 수 있을 것이다.

앞으로도 Docker를 사용할 때는 단순히 '동작하는' 컨테이너를 만드는 것을 넘어서 최적화된 이미지를 만들도록 노력해보자 👊

profile
코린이

0개의 댓글

관련 채용 정보